[{"data":1,"prerenderedAt":2354},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models":3,"surround:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models":2322,"ancestors:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models":2329,"children:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models":2334,"related:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models":2335},{"id":4,"title":5,"body":6,"dateModified":2288,"datePublished":2288,"description":2289,"extension":2290,"faq":2291,"keyword":2300,"kind":2301,"meta":2302,"navigation":119,"path":2311,"seo":2312,"slug":2315,"stem":2316,"topics":2317,"type":2320,"__hash__":2321},"docs\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models\u002Findex.md","Import an Excel File into Django Models",{"type":7,"value":8,"toc":2275},"minimark",[9,24,29,60,83,197,377,381,391,508,522,526,540,829,847,851,862,1026,1033,1246,1250,1253,1543,1561,1565,1568,1818,1825,1925,1929,1932,2130,2142,2146,2188,2192,2210,2214,2220,2226,2232,2238,2242,2271],[10,11,12,13,17,18,23],"p",{},"Sooner or later every Django application gets the request: \"can we upload the product list from Excel instead of typing it in?\" The first version reads the rows and calls ",[14,15,16],"code",{},"Product.objects.create"," in a loop, and it works until the spreadsheet contains a price with a comma, an empty name, a negative stock count or the same SKU twice — and then it fails half-way, leaving half the rows imported. A good import validates every row with the model's own rules before touching the database, reports problems by the row numbers the uploader sees in Excel, and writes all or nothing. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002F","Serving Excel Files from Python Web Apps",", builds that import against a real Django app.",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install django pandas openpyxl\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," django",[39,55,56],{"class":49}," pandas",[39,58,59],{"class":49}," openpyxl\n",[10,61,62,63,66,67,70,71,74,75,78,79,82],{},"The examples use a small Django app, ",[14,64,65],{},"catalog",", with a ",[14,68,69],{},"Category"," model and a ",[14,72,73],{},"Product"," model — SKU (unique), name, category (foreign key), price (decimal) and stock (positive integer) — on SQLite. ",[14,76,77],{},"products_upload.xlsx"," is the uploaded file: eight rows, with a price typed as ",[14,80,81],{},"129,00",", a blank name, a negative stock figure and one SKU listed twice.",[30,84,88],{"className":85,"code":86,"language":87,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import django\nfrom django.core.management import call_command\n\ndjango.setup()\ncall_command(\"migrate\", run_syncdb=True, verbosity=0)\nfrom catalog.models import Category, Product\nprint([f.name for f in Product._meta.get_fields()])\n","python",[14,89,90,100,114,121,127,163,176],{"__ignoreMap":35},[39,91,92,96],{"class":41,"line":42},[39,93,95],{"class":94},"s-kum","import",[39,97,99],{"class":98},"skGVy"," django\n",[39,101,103,106,109,111],{"class":41,"line":102},2,[39,104,105],{"class":94},"from",[39,107,108],{"class":98}," django.core.management ",[39,110,95],{"class":94},[39,112,113],{"class":98}," call_command\n",[39,115,117],{"class":41,"line":116},3,[39,118,120],{"emptyLinePlaceholder":119},true,"\n",[39,122,124],{"class":41,"line":123},4,[39,125,126],{"class":98},"django.setup()\n",[39,128,130,133,136,139,143,146,150,152,155,157,160],{"class":41,"line":129},5,[39,131,132],{"class":98},"call_command(",[39,134,135],{"class":49},"\"migrate\"",[39,137,138],{"class":98},", ",[39,140,142],{"class":141},"sa561","run_syncdb",[39,144,145],{"class":94},"=",[39,147,149],{"class":148},"sP0c6","True",[39,151,138],{"class":98},[39,153,154],{"class":141},"verbosity",[39,156,145],{"class":94},[39,158,159],{"class":148},"0",[39,161,162],{"class":98},")\n",[39,164,166,168,171,173],{"class":41,"line":165},6,[39,167,105],{"class":94},[39,169,170],{"class":98}," catalog.models ",[39,172,95],{"class":94},[39,174,175],{"class":98}," Category, Product\n",[39,177,179,182,185,188,191,194],{"class":41,"line":178},7,[39,180,181],{"class":148},"print",[39,183,184],{"class":98},"([f.name ",[39,186,187],{"class":94},"for",[39,189,190],{"class":98}," f ",[39,192,193],{"class":94},"in",[39,195,196],{"class":98}," Product._meta.get_fields()])\n",[198,199,207,208,207,212,207,216,207,223,207,233,207,244,207,251,207,256,207,262,207,267,207,271,207,278,207,284,207,288,207,292,207,296,207,299,207,302,207,308,207,312,207,315,207,319,207,322,207,327,207,334,207,340,207,343,207,346,207,350,207,354,207,357,207,362,207,369,207,374],"svg",{"viewBox":200,"role":201,"ariaLabelledBy":202,"xmlns":205,"style":206},"4 0 752 160","img",[203,204],"dj-flow-t","dj-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[209,210,211],"title",{"id":203},"A safe import in five stages",[213,214,215],"desc",{"id":204},"The uploaded workbook is read as text, each row is validated with the model's full_clean and collected as valid or invalid, duplicate keys are rejected, and only if every row is valid are related objects created and products upserted in one transaction; otherwise an error report goes back to the uploader.",[217,218],"rect",{"x":219,"y":159,"width":220,"height":221,"fill":222},"4","752","160","#ffffff",[217,224],{"x":225,"y":226,"width":227,"height":228,"rx":229,"fill":230,"stroke":231,"style":232},"20.0","16.0","115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[234,235,237],"g",{"transform":236},"translate(65.6,46.5) scale(1.000)",[238,239],"path",{"d":240,"fill":241,"stroke":242,"style":243},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[245,246,250],"text",{"x":247,"y":248,"style":249},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Read as text",[245,252,255],{"x":247,"y":253,"style":254},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","dtype=str",[41,257],{"x1":258,"y1":259,"x2":260,"y2":259,"stroke":261,"style":232},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[263,264],"polygon",{"points":265,"fill":266},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[217,268],{"x":269,"y":226,"width":227,"height":228,"rx":229,"fill":270,"stroke":261,"style":232},"171.2","#f0f4ff",[234,272,274],{"transform":273},"translate(216.8,37.8) scale(1.000)",[238,275],{"d":276,"fill":241,"stroke":277,"style":243},"M5 12.5l4.5 4.5L19 7","var(--brand-strong,#4338ca)",[245,279,283],{"x":280,"y":281,"style":282},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Validate",[245,285,287],{"x":280,"y":286,"style":282},"101.8","rows",[245,289,291],{"x":280,"y":290,"style":254},"121.2","full_clean()",[41,293],{"x1":294,"y1":259,"x2":295,"y2":259,"stroke":261,"style":232},"291.4","313.4",[263,297],{"points":298,"fill":266},"317.4,80.0 308.4,85.0 308.4,75.0",[217,300],{"x":301,"y":226,"width":227,"height":228,"rx":229,"fill":270,"stroke":261,"style":232},"322.4",[234,303,305],{"transform":304},"translate(368.0,46.5) scale(1.000)",[238,306],{"d":307,"fill":241,"stroke":277,"style":243},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[245,309,311],{"x":310,"y":248,"style":282},"380.0","Check keys",[245,313,314],{"x":310,"y":253,"style":254},"duplicate SKUs",[41,316],{"x1":317,"y1":259,"x2":318,"y2":259,"stroke":261,"style":232},"442.6","464.6",[263,320],{"points":321,"fill":266},"468.6,80.0 459.6,85.0 459.6,75.0",[217,323],{"x":324,"y":226,"width":227,"height":228,"rx":229,"fill":325,"stroke":326,"style":232},"473.6","#d9f4f1","var(--teal,#0f9488)",[234,328,330],{"transform":329},"translate(519.2,29.0) scale(1.000)",[238,331],{"d":332,"fill":241,"stroke":333,"style":243},"M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6 M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3","var(--teal-ink,#0b6157)",[245,335,339],{"x":336,"y":337,"style":338},"531.2","75.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Write",[245,341,342],{"x":336,"y":248,"style":338},"atomically",[245,344,345],{"x":336,"y":253,"style":254},"upsert in one",[245,347,349],{"x":336,"y":348,"style":254},"130.0","transaction",[41,351],{"x1":352,"y1":259,"x2":353,"y2":259,"stroke":261,"style":232},"593.8","615.8",[263,355],{"points":356,"fill":266},"619.8,80.0 610.8,85.0 610.8,75.0",[217,358],{"x":359,"y":226,"width":227,"height":228,"rx":229,"fill":360,"stroke":361,"style":232},"624.8","#fdefd8","var(--gold,#b4740a)",[234,363,365],{"transform":364},"translate(670.4,46.5) scale(1.000)",[238,366],{"d":367,"fill":241,"stroke":368,"style":243},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[245,370,373],{"x":371,"y":248,"style":372},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Report errors",[245,375,376],{"x":371,"y":253,"style":254},"by Excel row",[25,378,380],{"id":379},"step-1-read-every-cell-as-text","Step 1 — Read every cell as text",[10,382,383,384,386,387,390],{},"Let Django decide what a valid value is, not pandas. Reading with ",[14,385,255],{}," and ",[14,388,389],{},"keep_default_na=False"," delivers each cell as the text the uploader typed, blanks as empty strings:",[30,392,394],{"className":85,"code":393,"language":87,"meta":35,"style":35},"import pandas as pd\n\ndf = pd.read_excel(\"products_upload.xlsx\", dtype=str, keep_default_na=False)\ndf = df.apply(lambda col: col.str.strip())\ndf.insert(0, \"excel_row\", df.index + 2)          # header is row 1\nprint(df.to_string(index=False))\n",[14,395,396,409,413,448,463,491],{"__ignoreMap":35},[39,397,398,400,403,406],{"class":41,"line":42},[39,399,95],{"class":94},[39,401,402],{"class":98}," pandas ",[39,404,405],{"class":94},"as",[39,407,408],{"class":98}," pd\n",[39,410,411],{"class":41,"line":102},[39,412,120],{"emptyLinePlaceholder":119},[39,414,415,418,420,423,426,428,431,433,436,438,441,443,446],{"class":41,"line":116},[39,416,417],{"class":98},"df ",[39,419,145],{"class":94},[39,421,422],{"class":98}," pd.read_excel(",[39,424,425],{"class":49},"\"products_upload.xlsx\"",[39,427,138],{"class":98},[39,429,430],{"class":141},"dtype",[39,432,145],{"class":94},[39,434,435],{"class":148},"str",[39,437,138],{"class":98},[39,439,440],{"class":141},"keep_default_na",[39,442,145],{"class":94},[39,444,445],{"class":148},"False",[39,447,162],{"class":98},[39,449,450,452,454,457,460],{"class":41,"line":123},[39,451,417],{"class":98},[39,453,145],{"class":94},[39,455,456],{"class":98}," df.apply(",[39,458,459],{"class":94},"lambda",[39,461,462],{"class":98}," col: col.str.strip())\n",[39,464,465,468,470,472,475,478,481,484,487],{"class":41,"line":129},[39,466,467],{"class":98},"df.insert(",[39,469,159],{"class":148},[39,471,138],{"class":98},[39,473,474],{"class":49},"\"excel_row\"",[39,476,477],{"class":98},", df.index ",[39,479,480],{"class":94},"+",[39,482,483],{"class":148}," 2",[39,485,486],{"class":98},")          ",[39,488,490],{"class":489},"s-wDw","# header is row 1\n",[39,492,493,495,498,501,503,505],{"class":41,"line":165},[39,494,181],{"class":148},[39,496,497],{"class":98},"(df.to_string(",[39,499,500],{"class":141},"index",[39,502,145],{"class":94},[39,504,445],{"class":148},[39,506,507],{"class":98},"))\n",[10,509,510,511,513,514,517,518,521],{},"If pandas guessed the types, ",[14,512,81],{}," would make the whole price column text anyway while the other prices became floats, and an empty cell would become ",[14,515,516],{},"NaN",", which Django treats as a value rather than as \"blank\". The ",[14,519,520],{},"excel_row"," column records each record's row number in the spreadsheet — the index plus two, for the header and because Excel counts from one — so every error message can point at the row the uploader will look at.",[25,523,525],{"id":524},"step-2-validate-each-row-with-the-models-rules","Step 2 — Validate each row with the model's rules",[10,527,528,531,532,535,536,539],{},[14,529,530],{},"Model.full_clean()"," runs the same validation a Django form or the admin would: field types, lengths, ",[14,533,534],{},"blank",", choices, validators like ",[14,537,538],{},"PositiveIntegerField","'s minimum. Build an unsaved instance per row and collect the errors instead of stopping at the first:",[30,541,543],{"className":85,"code":542,"language":87,"meta":35,"style":35},"from django.core.exceptions import ValidationError\n\ndef validate_rows(frame):\n    valid, errors = [], []\n    for row in frame.itertuples(index=False):\n        product = Product(sku=row.SKU, name=row.Name, price=row.Price, stock=row.Stock)\n        try:\n            product.full_clean(exclude=[\"category\"], validate_unique=False)\n            valid.append((row.excel_row, row.Category, product))\n        except ValidationError as err:\n            for field, messages in err.message_dict.items():\n                errors.append({\"row\": row.excel_row, \"sku\": row.SKU, \"field\": field, \"problem\": \"; \".join(messages)})\n    return valid, errors\n\nvalid, errors = validate_rows(df)\nprint(len(valid), \"valid rows\")\nfor e in errors:\n    print(e)\n",[14,544,545,557,561,573,583,605,652,660,689,695,709,723,763,772,777,788,807,820],{"__ignoreMap":35},[39,546,547,549,552,554],{"class":41,"line":42},[39,548,105],{"class":94},[39,550,551],{"class":98}," django.core.exceptions ",[39,553,95],{"class":94},[39,555,556],{"class":98}," ValidationError\n",[39,558,559],{"class":41,"line":102},[39,560,120],{"emptyLinePlaceholder":119},[39,562,563,566,570],{"class":41,"line":116},[39,564,565],{"class":94},"def",[39,567,569],{"class":568},"s_Opv"," validate_rows",[39,571,572],{"class":98},"(frame):\n",[39,574,575,578,580],{"class":41,"line":123},[39,576,577],{"class":98},"    valid, errors ",[39,579,145],{"class":94},[39,581,582],{"class":98}," [], []\n",[39,584,585,588,591,593,596,598,600,602],{"class":41,"line":129},[39,586,587],{"class":94},"    for",[39,589,590],{"class":98}," row ",[39,592,193],{"class":94},[39,594,595],{"class":98}," frame.itertuples(",[39,597,500],{"class":141},[39,599,145],{"class":94},[39,601,445],{"class":148},[39,603,604],{"class":98},"):\n",[39,606,607,610,612,615,618,620,623,626,628,631,633,636,639,641,644,647,649],{"class":41,"line":165},[39,608,609],{"class":98},"        product ",[39,611,145],{"class":94},[39,613,614],{"class":98}," Product(",[39,616,617],{"class":141},"sku",[39,619,145],{"class":94},[39,621,622],{"class":98},"row.",[39,624,625],{"class":148},"SKU",[39,627,138],{"class":98},[39,629,630],{"class":141},"name",[39,632,145],{"class":94},[39,634,635],{"class":98},"row.Name, ",[39,637,638],{"class":141},"price",[39,640,145],{"class":94},[39,642,643],{"class":98},"row.Price, ",[39,645,646],{"class":141},"stock",[39,648,145],{"class":94},[39,650,651],{"class":98},"row.Stock)\n",[39,653,654,657],{"class":41,"line":178},[39,655,656],{"class":94},"        try",[39,658,659],{"class":98},":\n",[39,661,663,666,669,671,674,677,680,683,685,687],{"class":41,"line":662},8,[39,664,665],{"class":98},"            product.full_clean(",[39,667,668],{"class":141},"exclude",[39,670,145],{"class":94},[39,672,673],{"class":98},"[",[39,675,676],{"class":49},"\"category\"",[39,678,679],{"class":98},"], ",[39,681,682],{"class":141},"validate_unique",[39,684,145],{"class":94},[39,686,445],{"class":148},[39,688,162],{"class":98},[39,690,692],{"class":41,"line":691},9,[39,693,694],{"class":98},"            valid.append((row.excel_row, row.Category, product))\n",[39,696,698,701,704,706],{"class":41,"line":697},10,[39,699,700],{"class":94},"        except",[39,702,703],{"class":98}," ValidationError ",[39,705,405],{"class":94},[39,707,708],{"class":98}," err:\n",[39,710,712,715,718,720],{"class":41,"line":711},11,[39,713,714],{"class":94},"            for",[39,716,717],{"class":98}," field, messages ",[39,719,193],{"class":94},[39,721,722],{"class":98}," err.message_dict.items():\n",[39,724,726,729,732,735,738,741,743,745,748,751,754,757,760],{"class":41,"line":725},12,[39,727,728],{"class":98},"                errors.append({",[39,730,731],{"class":49},"\"row\"",[39,733,734],{"class":98},": row.excel_row, ",[39,736,737],{"class":49},"\"sku\"",[39,739,740],{"class":98},": row.",[39,742,625],{"class":148},[39,744,138],{"class":98},[39,746,747],{"class":49},"\"field\"",[39,749,750],{"class":98},": field, ",[39,752,753],{"class":49},"\"problem\"",[39,755,756],{"class":98},": ",[39,758,759],{"class":49},"\"; \"",[39,761,762],{"class":98},".join(messages)})\n",[39,764,766,769],{"class":41,"line":765},13,[39,767,768],{"class":94},"    return",[39,770,771],{"class":98}," valid, errors\n",[39,773,775],{"class":41,"line":774},14,[39,776,120],{"emptyLinePlaceholder":119},[39,778,780,783,785],{"class":41,"line":779},15,[39,781,782],{"class":98},"valid, errors ",[39,784,145],{"class":94},[39,786,787],{"class":98}," validate_rows(df)\n",[39,789,791,793,796,799,802,805],{"class":41,"line":790},16,[39,792,181],{"class":148},[39,794,795],{"class":98},"(",[39,797,798],{"class":148},"len",[39,800,801],{"class":98},"(valid), ",[39,803,804],{"class":49},"\"valid rows\"",[39,806,162],{"class":98},[39,808,810,812,815,817],{"class":41,"line":809},17,[39,811,187],{"class":94},[39,813,814],{"class":98}," e ",[39,816,193],{"class":94},[39,818,819],{"class":98}," errors:\n",[39,821,823,826],{"class":41,"line":822},18,[39,824,825],{"class":148},"    print",[39,827,828],{"class":98},"(e)\n",[10,830,831,832,834,835,838,839,842,843,846],{},"Three rows fail with Django's own messages: the price ",[14,833,81],{}," is not a decimal number, the name on row 6 cannot be blank, and the stock on row 7 must be at least 0. ",[14,836,837],{},"exclude=[\"category\"]"," skips the foreign key, which is resolved in a later step, and ",[14,840,841],{},"validate_unique=False"," skips the per-row database query for the SKU — uniqueness is handled for the whole file at once next. Validation that lives on the model is reused here for free; a custom ",[14,844,845],{},"clean()"," method on the model runs too.",[25,848,850],{"id":849},"step-3-reject-duplicate-keys-within-the-file","Step 3 — Reject duplicate keys within the file",[10,852,853,854,856,857,861],{},"The unique constraint on ",[14,855,617],{}," protects the database, but a SKU listed twice ",[858,859,860],"em",{},"in the upload"," is ambiguous — which row is right? Find those before writing and treat them as errors:",[30,863,865],{"className":85,"code":864,"language":87,"meta":35,"style":35},"dupes = df[df.duplicated(\"SKU\", keep=False)]\nfor row in dupes.itertuples(index=False):\n    errors.append({\"row\": row.excel_row, \"sku\": row.SKU, \"field\": \"sku\",\n                   \"problem\": f\"SKU appears more than once (rows {', '.join(map(str, dupes.loc[dupes['SKU'] == row.SKU, 'excel_row']))})\"})\nprint(sorted({e[\"row\"] for e in errors}))\n",[14,866,867,892,911,937,1001],{"__ignoreMap":35},[39,868,869,872,874,877,880,882,885,887,889],{"class":41,"line":42},[39,870,871],{"class":98},"dupes ",[39,873,145],{"class":94},[39,875,876],{"class":98}," df[df.duplicated(",[39,878,879],{"class":49},"\"SKU\"",[39,881,138],{"class":98},[39,883,884],{"class":141},"keep",[39,886,145],{"class":94},[39,888,445],{"class":148},[39,890,891],{"class":98},")]\n",[39,893,894,896,898,900,903,905,907,909],{"class":41,"line":102},[39,895,187],{"class":94},[39,897,590],{"class":98},[39,899,193],{"class":94},[39,901,902],{"class":98}," dupes.itertuples(",[39,904,500],{"class":141},[39,906,145],{"class":94},[39,908,445],{"class":148},[39,910,604],{"class":98},[39,912,913,916,918,920,922,924,926,928,930,932,934],{"class":41,"line":116},[39,914,915],{"class":98},"    errors.append({",[39,917,731],{"class":49},[39,919,734],{"class":98},[39,921,737],{"class":49},[39,923,740],{"class":98},[39,925,625],{"class":148},[39,927,138],{"class":98},[39,929,747],{"class":49},[39,931,756],{"class":98},[39,933,737],{"class":49},[39,935,936],{"class":98},",\n",[39,938,939,942,944,947,950,954,957,960,963,965,967,970,973,976,979,982,984,986,989,992,995,998],{"class":41,"line":123},[39,940,941],{"class":49},"                   \"problem\"",[39,943,756],{"class":98},[39,945,946],{"class":94},"f",[39,948,949],{"class":49},"\"SKU appears more than once (rows ",[39,951,953],{"class":952},"sSjpA","{",[39,955,956],{"class":49},"', '",[39,958,959],{"class":98},".join(",[39,961,962],{"class":148},"map",[39,964,795],{"class":98},[39,966,435],{"class":148},[39,968,969],{"class":98},", dupes.loc[dupes[",[39,971,972],{"class":49},"'SKU'",[39,974,975],{"class":98},"] ",[39,977,978],{"class":94},"==",[39,980,981],{"class":98}," row.",[39,983,625],{"class":148},[39,985,138],{"class":98},[39,987,988],{"class":49},"'excel_row'",[39,990,991],{"class":98},"]))",[39,993,994],{"class":952},"}",[39,996,997],{"class":49},")\"",[39,999,1000],{"class":98},"})\n",[39,1002,1003,1005,1007,1010,1013,1015,1017,1019,1021,1023],{"class":41,"line":129},[39,1004,181],{"class":148},[39,1006,795],{"class":98},[39,1008,1009],{"class":148},"sorted",[39,1011,1012],{"class":98},"({e[",[39,1014,731],{"class":49},[39,1016,975],{"class":98},[39,1018,187],{"class":94},[39,1020,814],{"class":98},[39,1022,193],{"class":94},[39,1024,1025],{"class":98}," errors}))\n",[10,1027,1028,1029,1032],{},"Both occurrences of ",[14,1030,1031],{},"SKU-110"," are reported, each naming the other's row, because the uploader has to choose between them. Guessing — \"the last one wins\" — silently discards data someone typed.",[198,1034,207,1040,207,1043,207,1046,207,1051,207,1057,207,1061,207,1067,207,1071,207,1075,207,1079,207,1083,207,1087,207,1091,207,1095,207,1100,207,1103,207,1107,207,1109,207,1111,207,1113,207,1116,207,1118,207,1121,207,1124,207,1128,207,1130,207,1134,207,1136,207,1139,207,1141,207,1143,207,1145,207,1148,207,1151,207,1154,207,1156,207,1159,207,1161,207,1164,207,1166,207,1168,207,1170,207,1173,207,1176,207,1179,207,1181,207,1184,207,1186,207,1189,207,1191,207,1193,207,1195,207,1198,207,1201,207,1204,207,1206,207,1209,207,1211,207,1214,207,1216,207,1218,207,1220,207,1223,207,1226,207,1229,207,1231,207,1234,207,1236,207,1238,207,1240,207,1242,207,1244],{"viewBox":1035,"role":201,"ariaLabelledBy":1036,"xmlns":205,"style":1039},"157 0 446 224",[1037,1038],"dj-errors-t","dj-errors-d","width:100%;max-width:446px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[209,1041,1042],{"id":1037},"The rows the import rejected",[213,1044,1045],{"id":1038},"A table of errors by Excel row number. Row 3 and row 8 share SKU-110; row 5 has a price with a decimal comma; row 6 has a blank name; row 7 has a negative stock.",[217,1047],{"x":1048,"y":159,"width":1049,"height":1050,"fill":222},"157","446","224",[217,1052],{"x":1053,"y":226,"width":1054,"height":1055,"rx":159,"fill":230,"stroke":231,"style":1056},"173.1","32.0","24.0","stroke-width:1px",[217,1058],{"x":1059,"y":226,"width":1060,"height":1055,"rx":159,"fill":230,"stroke":231,"style":1056},"205.1","47.6",[245,1062,1066],{"x":1063,"y":1064,"style":1065},"228.9","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[217,1068],{"x":1069,"y":226,"width":1070,"height":1055,"rx":159,"fill":230,"stroke":231,"style":1056},"252.7","81.6",[245,1072,1074],{"x":1073,"y":1064,"style":1065},"293.4","B",[217,1076],{"x":1077,"y":226,"width":1078,"height":1055,"rx":159,"fill":230,"stroke":231,"style":1056},"334.2","58.2",[245,1080,1082],{"x":1081,"y":1064,"style":1065},"363.3","C",[217,1084],{"x":1085,"y":226,"width":1086,"height":1055,"rx":159,"fill":230,"stroke":231,"style":1056},"392.4","194.5",[245,1088,1090],{"x":1089,"y":1064,"style":1065},"489.7","D",[217,1092],{"x":1053,"y":1093,"width":1054,"height":1094,"rx":159,"fill":230,"stroke":231,"style":1056},"40.0","28.0",[245,1096,1099],{"x":1097,"y":1098,"style":1065},"189.1","59.0","1",[217,1101],{"x":1059,"y":1093,"width":1060,"height":1094,"rx":159,"fill":270,"stroke":261,"style":1102},"stroke-width:1.5px",[245,1104,1106],{"x":1063,"y":1098,"style":1105},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","row",[217,1108],{"x":1069,"y":1093,"width":1070,"height":1094,"rx":159,"fill":270,"stroke":261,"style":1102},[245,1110,617],{"x":1073,"y":1098,"style":1105},[217,1112],{"x":1077,"y":1093,"width":1078,"height":1094,"rx":159,"fill":270,"stroke":261,"style":1102},[245,1114,1115],{"x":1081,"y":1098,"style":1105},"field",[217,1117],{"x":1085,"y":1093,"width":1086,"height":1094,"rx":159,"fill":270,"stroke":261,"style":1102},[245,1119,1120],{"x":1089,"y":1098,"style":1105},"problem",[217,1122],{"x":1053,"y":1123,"width":1054,"height":1094,"rx":159,"fill":230,"stroke":231,"style":1056},"68.0",[245,1125,1127],{"x":1097,"y":1126,"style":1065},"87.0","2",[217,1129],{"x":1059,"y":1123,"width":1060,"height":1094,"rx":159,"fill":360,"stroke":361,"style":1102},[245,1131,1133],{"x":1063,"y":1126,"style":1132},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","3",[217,1135],{"x":1069,"y":1123,"width":1070,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1137,1031],{"x":1073,"y":1126,"style":1138},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[217,1140],{"x":1077,"y":1123,"width":1078,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1142,617],{"x":1081,"y":1126,"style":1138},[217,1144],{"x":1085,"y":1123,"width":1086,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1146,1147],{"x":1089,"y":1126,"style":1138},"appears more than once",[217,1149],{"x":1053,"y":1150,"width":1054,"height":1094,"rx":159,"fill":230,"stroke":231,"style":1056},"96.0",[245,1152,1133],{"x":1097,"y":1153,"style":1065},"115.0",[217,1155],{"x":1059,"y":1150,"width":1060,"height":1094,"rx":159,"fill":360,"stroke":361,"style":1102},[245,1157,1158],{"x":1063,"y":1153,"style":1132},"5",[217,1160],{"x":1069,"y":1150,"width":1070,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1162,1163],{"x":1073,"y":1153,"style":1138},"SKU-210",[217,1165],{"x":1077,"y":1150,"width":1078,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1167,638],{"x":1081,"y":1153,"style":1138},[217,1169],{"x":1085,"y":1150,"width":1086,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1171,1172],{"x":1089,"y":1153,"style":1138},"“129,00” must be a decimal",[217,1174],{"x":1053,"y":1175,"width":1054,"height":1094,"rx":159,"fill":230,"stroke":231,"style":1056},"124.0",[245,1177,219],{"x":1097,"y":1178,"style":1065},"143.0",[217,1180],{"x":1059,"y":1175,"width":1060,"height":1094,"rx":159,"fill":360,"stroke":361,"style":1102},[245,1182,1183],{"x":1063,"y":1178,"style":1132},"6",[217,1185],{"x":1069,"y":1175,"width":1070,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1187,1188],{"x":1073,"y":1178,"style":1138},"SKU-300",[217,1190],{"x":1077,"y":1175,"width":1078,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1192,630],{"x":1081,"y":1178,"style":1138},[217,1194],{"x":1085,"y":1175,"width":1086,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1196,1197],{"x":1089,"y":1178,"style":1138},"cannot be blank",[217,1199],{"x":1053,"y":1200,"width":1054,"height":1094,"rx":159,"fill":230,"stroke":231,"style":1056},"152.0",[245,1202,1158],{"x":1097,"y":1203,"style":1065},"171.0",[217,1205],{"x":1059,"y":1200,"width":1060,"height":1094,"rx":159,"fill":360,"stroke":361,"style":1102},[245,1207,1208],{"x":1063,"y":1203,"style":1132},"7",[217,1210],{"x":1069,"y":1200,"width":1070,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1212,1213],{"x":1073,"y":1203,"style":1138},"SKU-310",[217,1215],{"x":1077,"y":1200,"width":1078,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1217,646],{"x":1081,"y":1203,"style":1138},[217,1219],{"x":1085,"y":1200,"width":1086,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1221,1222],{"x":1089,"y":1203,"style":1138},"must be ≥ 0",[217,1224],{"x":1053,"y":1225,"width":1054,"height":1094,"rx":159,"fill":230,"stroke":231,"style":1056},"180.0",[245,1227,1183],{"x":1097,"y":1228,"style":1065},"199.0",[217,1230],{"x":1059,"y":1225,"width":1060,"height":1094,"rx":159,"fill":360,"stroke":361,"style":1102},[245,1232,1233],{"x":1063,"y":1228,"style":1132},"8",[217,1235],{"x":1069,"y":1225,"width":1070,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1237,1031],{"x":1073,"y":1228,"style":1138},[217,1239],{"x":1077,"y":1225,"width":1078,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1241,617],{"x":1081,"y":1228,"style":1138},[217,1243],{"x":1085,"y":1225,"width":1086,"height":1094,"rx":159,"fill":222,"stroke":231,"style":1056},[245,1245,1147],{"x":1089,"y":1228,"style":1138},[25,1247,1249],{"id":1248},"step-4-all-or-nothing","Step 4 — All or nothing",[10,1251,1252],{},"If any row failed, import nothing and send the errors back; a half-imported product list is worse than none. When everything is valid, create missing categories and upsert the products in one transaction:",[30,1254,1256],{"className":85,"code":1255,"language":87,"meta":35,"style":35},"from django.db import transaction\n\ndef import_products(valid):\n    with transaction.atomic():\n        names = {cat for _, cat, _ in valid}\n        categories = {c.name: c for c in Category.objects.filter(name__in=names)}\n        for name in names - set(categories):\n            categories[name] = Category.objects.create(name=name)\n        products = []\n        for _, cat, product in valid:\n            product.category = categories[cat]\n            products.append(product)\n        Product.objects.bulk_create(products, update_conflicts=True, unique_fields=[\"sku\"],\n                                    update_fields=[\"name\", \"category\", \"price\", \"stock\"])\n    return len(products)\n\nif errors:\n    print(f\"{len({e['row'] for e in errors})} rows have problems — nothing imported\")\nprint(\"products in database:\", Product.objects.count())\n",[14,1257,1258,1270,1274,1284,1292,1312,1340,1362,1379,1389,1401,1411,1416,1442,1471,1481,1485,1492,1530],{"__ignoreMap":35},[39,1259,1260,1262,1265,1267],{"class":41,"line":42},[39,1261,105],{"class":94},[39,1263,1264],{"class":98}," django.db ",[39,1266,95],{"class":94},[39,1268,1269],{"class":98}," transaction\n",[39,1271,1272],{"class":41,"line":102},[39,1273,120],{"emptyLinePlaceholder":119},[39,1275,1276,1278,1281],{"class":41,"line":116},[39,1277,565],{"class":94},[39,1279,1280],{"class":568}," import_products",[39,1282,1283],{"class":98},"(valid):\n",[39,1285,1286,1289],{"class":41,"line":123},[39,1287,1288],{"class":94},"    with",[39,1290,1291],{"class":98}," transaction.atomic():\n",[39,1293,1294,1297,1299,1302,1304,1307,1309],{"class":41,"line":129},[39,1295,1296],{"class":98},"        names ",[39,1298,145],{"class":94},[39,1300,1301],{"class":98}," {cat ",[39,1303,187],{"class":94},[39,1305,1306],{"class":98}," _, cat, _ ",[39,1308,193],{"class":94},[39,1310,1311],{"class":98}," valid}\n",[39,1313,1314,1317,1319,1322,1324,1327,1329,1332,1335,1337],{"class":41,"line":165},[39,1315,1316],{"class":98},"        categories ",[39,1318,145],{"class":94},[39,1320,1321],{"class":98}," {c.name: c ",[39,1323,187],{"class":94},[39,1325,1326],{"class":98}," c ",[39,1328,193],{"class":94},[39,1330,1331],{"class":98}," Category.objects.filter(",[39,1333,1334],{"class":141},"name__in",[39,1336,145],{"class":94},[39,1338,1339],{"class":98},"names)}\n",[39,1341,1342,1345,1348,1350,1353,1356,1359],{"class":41,"line":178},[39,1343,1344],{"class":94},"        for",[39,1346,1347],{"class":98}," name ",[39,1349,193],{"class":94},[39,1351,1352],{"class":98}," names ",[39,1354,1355],{"class":94},"-",[39,1357,1358],{"class":148}," set",[39,1360,1361],{"class":98},"(categories):\n",[39,1363,1364,1367,1369,1372,1374,1376],{"class":41,"line":662},[39,1365,1366],{"class":98},"            categories[name] ",[39,1368,145],{"class":94},[39,1370,1371],{"class":98}," Category.objects.create(",[39,1373,630],{"class":141},[39,1375,145],{"class":94},[39,1377,1378],{"class":98},"name)\n",[39,1380,1381,1384,1386],{"class":41,"line":691},[39,1382,1383],{"class":98},"        products ",[39,1385,145],{"class":94},[39,1387,1388],{"class":98}," []\n",[39,1390,1391,1393,1396,1398],{"class":41,"line":697},[39,1392,1344],{"class":94},[39,1394,1395],{"class":98}," _, cat, product ",[39,1397,193],{"class":94},[39,1399,1400],{"class":98}," valid:\n",[39,1402,1403,1406,1408],{"class":41,"line":711},[39,1404,1405],{"class":98},"            product.category ",[39,1407,145],{"class":94},[39,1409,1410],{"class":98}," categories[cat]\n",[39,1412,1413],{"class":41,"line":725},[39,1414,1415],{"class":98},"            products.append(product)\n",[39,1417,1418,1421,1424,1426,1428,1430,1433,1435,1437,1439],{"class":41,"line":765},[39,1419,1420],{"class":98},"        Product.objects.bulk_create(products, ",[39,1422,1423],{"class":141},"update_conflicts",[39,1425,145],{"class":94},[39,1427,149],{"class":148},[39,1429,138],{"class":98},[39,1431,1432],{"class":141},"unique_fields",[39,1434,145],{"class":94},[39,1436,673],{"class":98},[39,1438,737],{"class":49},[39,1440,1441],{"class":98},"],\n",[39,1443,1444,1447,1449,1451,1454,1456,1458,1460,1463,1465,1468],{"class":41,"line":774},[39,1445,1446],{"class":141},"                                    update_fields",[39,1448,145],{"class":94},[39,1450,673],{"class":98},[39,1452,1453],{"class":49},"\"name\"",[39,1455,138],{"class":98},[39,1457,676],{"class":49},[39,1459,138],{"class":98},[39,1461,1462],{"class":49},"\"price\"",[39,1464,138],{"class":98},[39,1466,1467],{"class":49},"\"stock\"",[39,1469,1470],{"class":98},"])\n",[39,1472,1473,1475,1478],{"class":41,"line":779},[39,1474,768],{"class":94},[39,1476,1477],{"class":148}," len",[39,1479,1480],{"class":98},"(products)\n",[39,1482,1483],{"class":41,"line":790},[39,1484,120],{"emptyLinePlaceholder":119},[39,1486,1487,1490],{"class":41,"line":809},[39,1488,1489],{"class":94},"if",[39,1491,819],{"class":98},[39,1493,1494,1496,1498,1500,1503,1505,1507,1509,1512,1514,1516,1518,1520,1523,1525,1528],{"class":41,"line":822},[39,1495,825],{"class":148},[39,1497,795],{"class":98},[39,1499,946],{"class":94},[39,1501,1502],{"class":49},"\"",[39,1504,953],{"class":952},[39,1506,798],{"class":148},[39,1508,1012],{"class":98},[39,1510,1511],{"class":49},"'row'",[39,1513,975],{"class":98},[39,1515,187],{"class":94},[39,1517,814],{"class":98},[39,1519,193],{"class":94},[39,1521,1522],{"class":98}," errors})",[39,1524,994],{"class":952},[39,1526,1527],{"class":49}," rows have problems — nothing imported\"",[39,1529,162],{"class":98},[39,1531,1533,1535,1537,1540],{"class":41,"line":1532},19,[39,1534,181],{"class":148},[39,1536,795],{"class":98},[39,1538,1539],{"class":49},"\"products in database:\"",[39,1541,1542],{"class":98},", Product.objects.count())\n",[10,1544,1545,1548,1549,1552,1553,1556,1557,1560],{},[14,1546,1547],{},"transaction.atomic()"," makes the whole import one unit: if anything inside raises, every change is rolled back. ",[14,1550,1551],{},"bulk_create"," with ",[14,1554,1555],{},"update_conflicts=True"," is an ",[858,1558,1559],{},"upsert"," — insert new SKUs, update the named fields of existing ones — in a single statement on databases that support it, including PostgreSQL, SQLite and MySQL\u002FMariaDB. Categories are looked up once for all names and created only if missing, instead of one query per row.",[25,1562,1564],{"id":1563},"step-5-import-the-corrected-file","Step 5 — Import the corrected file",[10,1566,1567],{},"The uploader fixes the five rows — the decimal point, the name, the stock, and keeps one of the two SKU-110 rows — and uploads again. This time every row is valid and the import runs:",[30,1569,1571],{"className":85,"code":1570,"language":87,"meta":35,"style":35},"fixed = df[df[\"excel_row\"] != 3].copy()\nfixed.loc[fixed[\"SKU\"] == \"SKU-210\", \"Price\"] = \"129.00\"\nfixed.loc[fixed[\"SKU\"] == \"SKU-300\", \"Name\"] = \"Brass floor lamp\"\nfixed.loc[fixed[\"SKU\"] == \"SKU-310\", \"Stock\"] = \"2\"\nvalid, errors = validate_rows(fixed)\nassert not errors and not fixed[\"SKU\"].duplicated().any()\nprint(import_products(valid), \"products imported\")\nprint(list(Product.objects.order_by(\"sku\").values_list(\"sku\", \"category__name\", \"price\", \"stock\")))\n\nagain = fixed.assign(Stock=\"0\")\nprint(import_products(validate_rows(again)[0]), \"rows upserted a second time;\", Product.objects.count(), \"products,\",\n      Product.objects.filter(stock=0).count(), \"now with stock 0\")\n",[14,1572,1573,1596,1622,1647,1672,1681,1705,1717,1752,1756,1776,1799],{"__ignoreMap":35},[39,1574,1575,1578,1580,1583,1585,1587,1590,1593],{"class":41,"line":42},[39,1576,1577],{"class":98},"fixed ",[39,1579,145],{"class":94},[39,1581,1582],{"class":98}," df[df[",[39,1584,474],{"class":49},[39,1586,975],{"class":98},[39,1588,1589],{"class":94},"!=",[39,1591,1592],{"class":148}," 3",[39,1594,1595],{"class":98},"].copy()\n",[39,1597,1598,1601,1603,1605,1607,1610,1612,1615,1617,1619],{"class":41,"line":102},[39,1599,1600],{"class":98},"fixed.loc[fixed[",[39,1602,879],{"class":49},[39,1604,975],{"class":98},[39,1606,978],{"class":94},[39,1608,1609],{"class":49}," \"SKU-210\"",[39,1611,138],{"class":98},[39,1613,1614],{"class":49},"\"Price\"",[39,1616,975],{"class":98},[39,1618,145],{"class":94},[39,1620,1621],{"class":49}," \"129.00\"\n",[39,1623,1624,1626,1628,1630,1632,1635,1637,1640,1642,1644],{"class":41,"line":116},[39,1625,1600],{"class":98},[39,1627,879],{"class":49},[39,1629,975],{"class":98},[39,1631,978],{"class":94},[39,1633,1634],{"class":49}," \"SKU-300\"",[39,1636,138],{"class":98},[39,1638,1639],{"class":49},"\"Name\"",[39,1641,975],{"class":98},[39,1643,145],{"class":94},[39,1645,1646],{"class":49}," \"Brass floor lamp\"\n",[39,1648,1649,1651,1653,1655,1657,1660,1662,1665,1667,1669],{"class":41,"line":123},[39,1650,1600],{"class":98},[39,1652,879],{"class":49},[39,1654,975],{"class":98},[39,1656,978],{"class":94},[39,1658,1659],{"class":49}," \"SKU-310\"",[39,1661,138],{"class":98},[39,1663,1664],{"class":49},"\"Stock\"",[39,1666,975],{"class":98},[39,1668,145],{"class":94},[39,1670,1671],{"class":49}," \"2\"\n",[39,1673,1674,1676,1678],{"class":41,"line":129},[39,1675,782],{"class":98},[39,1677,145],{"class":94},[39,1679,1680],{"class":98}," validate_rows(fixed)\n",[39,1682,1683,1686,1689,1692,1695,1697,1700,1702],{"class":41,"line":165},[39,1684,1685],{"class":94},"assert",[39,1687,1688],{"class":94}," not",[39,1690,1691],{"class":98}," errors ",[39,1693,1694],{"class":94},"and",[39,1696,1688],{"class":94},[39,1698,1699],{"class":98}," fixed[",[39,1701,879],{"class":49},[39,1703,1704],{"class":98},"].duplicated().any()\n",[39,1706,1707,1709,1712,1715],{"class":41,"line":178},[39,1708,181],{"class":148},[39,1710,1711],{"class":98},"(import_products(valid), ",[39,1713,1714],{"class":49},"\"products imported\"",[39,1716,162],{"class":98},[39,1718,1719,1721,1723,1726,1729,1731,1734,1736,1738,1741,1743,1745,1747,1749],{"class":41,"line":662},[39,1720,181],{"class":148},[39,1722,795],{"class":98},[39,1724,1725],{"class":148},"list",[39,1727,1728],{"class":98},"(Product.objects.order_by(",[39,1730,737],{"class":49},[39,1732,1733],{"class":98},").values_list(",[39,1735,737],{"class":49},[39,1737,138],{"class":98},[39,1739,1740],{"class":49},"\"category__name\"",[39,1742,138],{"class":98},[39,1744,1462],{"class":49},[39,1746,138],{"class":98},[39,1748,1467],{"class":49},[39,1750,1751],{"class":98},")))\n",[39,1753,1754],{"class":41,"line":691},[39,1755,120],{"emptyLinePlaceholder":119},[39,1757,1758,1761,1763,1766,1769,1771,1774],{"class":41,"line":697},[39,1759,1760],{"class":98},"again ",[39,1762,145],{"class":94},[39,1764,1765],{"class":98}," fixed.assign(",[39,1767,1768],{"class":141},"Stock",[39,1770,145],{"class":94},[39,1772,1773],{"class":49},"\"0\"",[39,1775,162],{"class":98},[39,1777,1778,1780,1783,1785,1788,1791,1794,1797],{"class":41,"line":711},[39,1779,181],{"class":148},[39,1781,1782],{"class":98},"(import_products(validate_rows(again)[",[39,1784,159],{"class":148},[39,1786,1787],{"class":98},"]), ",[39,1789,1790],{"class":49},"\"rows upserted a second time;\"",[39,1792,1793],{"class":98},", Product.objects.count(), ",[39,1795,1796],{"class":49},"\"products,\"",[39,1798,936],{"class":98},[39,1800,1801,1804,1806,1808,1810,1813,1816],{"class":41,"line":725},[39,1802,1803],{"class":98},"      Product.objects.filter(",[39,1805,646],{"class":141},[39,1807,145],{"class":94},[39,1809,159],{"class":148},[39,1811,1812],{"class":98},").count(), ",[39,1814,1815],{"class":49},"\"now with stock 0\"",[39,1817,162],{"class":98},[10,1819,1820,1821,1824],{},"Seven products are created along with the four categories. Importing the same file again with different stock figures updates the existing products instead of failing on the unique constraint or creating duplicates — the count stays at seven. That is what makes the import safe to run every week with the latest spreadsheet. In production, wrap the same functions in a management command — ",[14,1822,1823],{},"python manage.py import_products products.xlsx"," — so scheduled imports and uploads through a view share one tested code path.",[198,1826,207,1831,207,1834,207,1837,207,1840,207,1845,207,1851,207,1857,207,1862,207,1868,207,1874,207,1878,207,1883,207,1887,207,1890,207,1896,207,1901,207,1905,207,1909,207,1914,207,1917,207,1922],{"viewBox":1827,"role":201,"ariaLabelledBy":1828,"xmlns":205,"style":206},"4 0 752 207",[1829,1830],"dj-runs-t","dj-runs-d",[209,1832,1833],{"id":1829},"The same import, first and second time",[213,1835,1836],{"id":1830},"Two panels. On the first run seven products and four categories are created. On the second run with changed stock figures the same seven products are updated in place and no rows are added.",[217,1838],{"x":219,"y":159,"width":220,"height":1839,"fill":222},"207",[217,1841],{"x":225,"y":226,"width":1842,"height":1843,"rx":1844,"fill":270,"stroke":261,"style":232},"351.0","175.0","14",[234,1846,1848],{"transform":1847},"translate(182.5,29.0) scale(1.083)",[238,1849],{"d":332,"fill":241,"stroke":277,"style":1850},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[245,1852,1856],{"x":1853,"y":1854,"style":1855},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","First upload",[41,1858],{"x1":1859,"y1":1860,"x2":1861,"y2":1860,"stroke":261,"style":1056},"36.0","91.0","355.0",[245,1863,1867],{"x":1864,"y":1865,"style":1866},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","4 categories created",[234,1869,1871],{"transform":1870},"translate(38.5,99.5) scale(0.625)",[238,1872],{"d":276,"fill":241,"stroke":326,"style":1873},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[245,1875,1877],{"x":1864,"y":1876,"style":1866},"137.0","7 products inserted",[234,1879,1881],{"transform":1880},"translate(38.5,125.5) scale(0.625)",[238,1882],{"d":276,"fill":241,"stroke":326,"style":1873},[245,1884,1886],{"x":1864,"y":1885,"style":1866},"163.0","all in one transaction",[217,1888],{"x":1889,"y":226,"width":1842,"height":1843,"rx":1844,"fill":325,"stroke":326,"style":232},"389.0",[234,1891,1893],{"transform":1892},"translate(551.5,29.0) scale(1.083)",[238,1894],{"d":1895,"fill":241,"stroke":333,"style":1850},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[245,1897,1900],{"x":1898,"y":1854,"style":1899},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Weekly re-upload",[41,1902],{"x1":1903,"y1":1860,"x2":1904,"y2":1860,"stroke":326,"style":1056},"405.0","724.0",[245,1906,1908],{"x":1907,"y":1865,"style":1866},"429.0","7 products updated",[234,1910,1912],{"transform":1911},"translate(407.5,99.5) scale(0.625)",[238,1913],{"d":276,"fill":241,"stroke":326,"style":1873},[245,1915,1916],{"x":1907,"y":1876,"style":1866},"no duplicates, no errors",[234,1918,1920],{"transform":1919},"translate(407.5,125.5) scale(0.625)",[238,1921],{"d":276,"fill":241,"stroke":326,"style":1873},[245,1923,1924],{"x":1907,"y":1885,"style":1866},"count stays at 7",[25,1926,1928],{"id":1927},"step-6-send-the-errors-back-as-a-workbook","Step 6 — Send the errors back as a workbook",[10,1930,1931],{},"People fix spreadsheets in Excel, so return the problems in Excel: the original rows with an error column, sorted by row number:",[30,1933,1935],{"className":85,"code":1934,"language":87,"meta":35,"style":35},"valid, errors = validate_rows(df)\ndupes = df[df.duplicated(\"SKU\", keep=False)]\nerrors += [{\"row\": r.excel_row, \"sku\": r.SKU, \"field\": \"sku\", \"problem\": \"SKU appears more than once\"}\n           for r in dupes.itertuples(index=False)]\nproblems = (pd.DataFrame(errors).groupby(\"row\")[\"problem\"].apply(\"; \".join).rename(\"Problem\"))\nreport = df.set_index(\"excel_row\").join(problems, how=\"inner\").reset_index()\nreport.to_excel(\"products_upload_errors.xlsx\", index=False)\nprint(report[[\"excel_row\", \"SKU\", \"Problem\"]].to_string(index=False))\n",[14,1936,1937,1945,1965,2008,2028,2058,2084,2102],{"__ignoreMap":35},[39,1938,1939,1941,1943],{"class":41,"line":42},[39,1940,782],{"class":98},[39,1942,145],{"class":94},[39,1944,787],{"class":98},[39,1946,1947,1949,1951,1953,1955,1957,1959,1961,1963],{"class":41,"line":102},[39,1948,871],{"class":98},[39,1950,145],{"class":94},[39,1952,876],{"class":98},[39,1954,879],{"class":49},[39,1956,138],{"class":98},[39,1958,884],{"class":141},[39,1960,145],{"class":94},[39,1962,445],{"class":148},[39,1964,891],{"class":98},[39,1966,1967,1970,1973,1976,1978,1981,1983,1986,1988,1990,1992,1994,1996,1998,2000,2002,2005],{"class":41,"line":116},[39,1968,1969],{"class":98},"errors ",[39,1971,1972],{"class":94},"+=",[39,1974,1975],{"class":98}," [{",[39,1977,731],{"class":49},[39,1979,1980],{"class":98},": r.excel_row, ",[39,1982,737],{"class":49},[39,1984,1985],{"class":98},": r.",[39,1987,625],{"class":148},[39,1989,138],{"class":98},[39,1991,747],{"class":49},[39,1993,756],{"class":98},[39,1995,737],{"class":49},[39,1997,138],{"class":98},[39,1999,753],{"class":49},[39,2001,756],{"class":98},[39,2003,2004],{"class":49},"\"SKU appears more than once\"",[39,2006,2007],{"class":98},"}\n",[39,2009,2010,2013,2016,2018,2020,2022,2024,2026],{"class":41,"line":123},[39,2011,2012],{"class":94},"           for",[39,2014,2015],{"class":98}," r ",[39,2017,193],{"class":94},[39,2019,902],{"class":98},[39,2021,500],{"class":141},[39,2023,145],{"class":94},[39,2025,445],{"class":148},[39,2027,891],{"class":98},[39,2029,2030,2033,2035,2038,2040,2043,2045,2048,2050,2053,2056],{"class":41,"line":129},[39,2031,2032],{"class":98},"problems ",[39,2034,145],{"class":94},[39,2036,2037],{"class":98}," (pd.DataFrame(errors).groupby(",[39,2039,731],{"class":49},[39,2041,2042],{"class":98},")[",[39,2044,753],{"class":49},[39,2046,2047],{"class":98},"].apply(",[39,2049,759],{"class":49},[39,2051,2052],{"class":98},".join).rename(",[39,2054,2055],{"class":49},"\"Problem\"",[39,2057,507],{"class":98},[39,2059,2060,2063,2065,2068,2070,2073,2076,2078,2081],{"class":41,"line":165},[39,2061,2062],{"class":98},"report ",[39,2064,145],{"class":94},[39,2066,2067],{"class":98}," df.set_index(",[39,2069,474],{"class":49},[39,2071,2072],{"class":98},").join(problems, ",[39,2074,2075],{"class":141},"how",[39,2077,145],{"class":94},[39,2079,2080],{"class":49},"\"inner\"",[39,2082,2083],{"class":98},").reset_index()\n",[39,2085,2086,2089,2092,2094,2096,2098,2100],{"class":41,"line":178},[39,2087,2088],{"class":98},"report.to_excel(",[39,2090,2091],{"class":49},"\"products_upload_errors.xlsx\"",[39,2093,138],{"class":98},[39,2095,500],{"class":141},[39,2097,145],{"class":94},[39,2099,445],{"class":148},[39,2101,162],{"class":98},[39,2103,2104,2106,2109,2111,2113,2115,2117,2119,2122,2124,2126,2128],{"class":41,"line":662},[39,2105,181],{"class":148},[39,2107,2108],{"class":98},"(report[[",[39,2110,474],{"class":49},[39,2112,138],{"class":98},[39,2114,879],{"class":49},[39,2116,138],{"class":98},[39,2118,2055],{"class":49},[39,2120,2121],{"class":98},"]].to_string(",[39,2123,500],{"class":141},[39,2125,145],{"class":94},[39,2127,445],{"class":148},[39,2129,507],{"class":98},[10,2131,2132,2133,2137,2138,2141],{},"In a web view this workbook is returned as a download right after the failed upload — ",[19,2134,2136],{"href":2135},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Freturn-an-excel-file-from-a-flask-download-endpoint\u002F","Return an Excel File from a Flask Download Endpoint"," shows the response side, and the same ",[14,2139,2140],{},"HttpResponse"," pattern works in Django.",[25,2143,2145],{"id":2144},"common-pitfalls","Common pitfalls",[2147,2148,2149,2160,2166,2172,2178],"ul",{},[2150,2151,2152,2156,2157,2159],"li",{},[2153,2154,2155],"strong",{},"Creating rows one by one without a transaction."," A failure half-way leaves half an import; wrap it in ",[14,2158,1547],{},".",[2150,2161,2162,2165],{},[2153,2163,2164],{},"Letting pandas guess types."," Read as text and let the model validate.",[2150,2167,2168,2171],{},[2153,2169,2170],{},"Error messages with pandas index numbers."," Report the Excel row number the uploader sees.",[2150,2173,2174,2177],{},[2153,2175,2176],{},"\"Last one wins\" for duplicate keys."," Report duplicates and let a person choose.",[2150,2179,2180,2183,2184,2159],{},[2153,2181,2182],{},"Trusting uploaded files blindly."," Check size and type before reading; see ",[19,2185,2187],{"href":2186},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app\u002F","Validate an Uploaded Excel File in a Web App",[25,2189,2191],{"id":2190},"conclusion","Conclusion",[10,2193,2194,2195,386,2197,2199,2200,2202,2203,2206,2207,2209],{},"Read the upload with ",[14,2196,255],{},[14,2198,389],{},", number each record by its Excel row, validate each row with the model's ",[14,2201,291],{}," and collect every error, and reject duplicate keys inside the file. Import only when every row is valid — related objects looked up or created in bulk, products upserted with ",[14,2204,2205],{},"bulk_create(update_conflicts=True)"," inside ",[14,2208,1547],{}," — and otherwise return a workbook of the problems keyed by row number.",[25,2211,2213],{"id":2212},"frequently-asked-questions","Frequently asked questions",[10,2215,2216,2219],{},[2153,2217,2218],{},"How do I import an Excel file into a Django model?","\nRead the file with pandas, build an unsaved model instance per row, validate it with full_clean(), and save the valid instances inside transaction.atomic(), for example with bulk_create. Report invalid rows back to the user.",[10,2221,2222,2225],{},[2153,2223,2224],{},"How do I update existing records instead of creating duplicates?","\nUse bulk_create with update_conflicts=True, unique_fields set to the natural key such as sku, and update_fields listing the columns to overwrite. Existing rows are updated and new ones inserted in one statement.",[10,2227,2228,2231],{},[2153,2229,2230],{},"How do I validate Excel rows with Django's rules?","\nCreate a model instance from each row and call full_clean(). It runs field validation, blank and length checks, validators and the model's clean method, raising ValidationError with a message per field.",[10,2233,2234,2237],{},[2153,2235,2236],{},"How do I show users which rows failed?","\nKeep each record's spreadsheet row number — the pandas index plus two — and include it in every error. Return the failing rows with an error column as an Excel file they can fix and re-upload.",[25,2239,2241],{"id":2240},"related","Related",[2147,2243,2244,2250,2257,2264],{},[2150,2245,2246,2247,2249],{},"Up one level: ",[19,2248,22],{"href":21}," — Excel in and out of web applications.",[2150,2251,2252,2256],{},[19,2253,2255],{"href":2254},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel\u002F","Export a Django Queryset to Excel"," — the opposite direction.",[2150,2258,2259,2263],{},[19,2260,2262],{"href":2261},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fhandle-an-uploaded-excel-file-in-flask-and-fastapi\u002F","Handle an Uploaded Excel File in Flask and FastAPI"," — receiving the upload.",[2150,2265,2266,2270],{},[19,2267,2269],{"href":2268},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic\u002F","Validate Excel Rows with Pydantic"," — the same row-by-row validation outside Django.",[2272,2273,2274],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":102,"depth":102,"links":2276},[2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287],{"id":27,"depth":102,"text":28},{"id":379,"depth":102,"text":380},{"id":524,"depth":102,"text":525},{"id":849,"depth":102,"text":850},{"id":1248,"depth":102,"text":1249},{"id":1563,"depth":102,"text":1564},{"id":1927,"depth":102,"text":1928},{"id":2144,"depth":102,"text":2145},{"id":2190,"depth":102,"text":2191},{"id":2212,"depth":102,"text":2213},{"id":2240,"depth":102,"text":2241},"2026-09-29","Load a spreadsheet of products, customers or prices into a Django database the safe way: read every cell as text, validate each row with the model's own full_clean, report errors by Excel row number, reject duplicate keys in the file, create missing related objects, upsert everything in one transaction with bulk_create, and return an error workbook the uploader can fix.","md",[2292,2294,2296,2298],{"q":2218,"a":2293},"Read the file with pandas, build an unsaved model instance per row, validate it with full_clean(), and save the valid instances inside transaction.atomic(), for example with bulk_create. Report invalid rows back to the user.",{"q":2224,"a":2295},"Use bulk_create with update_conflicts=True, unique_fields set to the natural key such as sku, and update_fields listing the columns to overwrite. Existing rows are updated and new ones inserted in one statement.",{"q":2230,"a":2297},"Create a model instance from each row and call full_clean(). It runs field validation, blank and length checks, validators and the model's clean method, raising ValidationError with a message per field.",{"q":2236,"a":2299},"Keep each record's spreadsheet row number — the pandas index plus two — and include it in every error. Return the failing rows with an error column as an Excel file they can fix and re-upload.","import an excel file into django models",null,{"breadcrumb":2303},[2304,2307,2310],{"name":2305,"item":2306},"Home","\u002F",{"name":2308,"item":2309},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models",{"title":2313,"description":2314},"Import Excel Data into Django Models with Validation","Import an Excel file into Django: pandas dtype=str, model.full_clean() per row, errors mapped to spreadsheet row numbers, duplicate keys, foreign keys via get_or_create, transaction.atomic and bulk_create(update_conflicts=True) upserts, and an error report workbook.","import-an-excel-file-into-django-models","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models\u002Findex",[2318,2319],"web-apps","data-validation","how-to","zgnCb5OkXq8JTXCWYJwBJH-FSdjk4HYF9dbt37_qVkI",[2323,2326],{"title":2262,"path":2324,"stem":2325,"children":-1},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fhandle-an-uploaded-excel-file-in-flask-and-fastapi","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fhandle-an-uploaded-excel-file-in-flask-and-fastapi\u002Findex",{"title":2136,"path":2327,"stem":2328,"children":-1},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Freturn-an-excel-file-from-a-flask-download-endpoint","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Freturn-an-excel-file-from-a-flask-download-endpoint\u002Findex",[2330,2332],{"path":2331,"title":2308},"\u002Fautomating-reporting-workflows",{"path":2333,"title":22},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps",[],[2336,2339,2342,2345,2348,2351],{"path":2337,"title":2338,"kind":2320},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat","Schedule Excel Reports with Celery Beat",{"path":2340,"title":2341,"kind":2320},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Ftrigger-an-excel-report-from-a-webhook","Trigger an Excel Report from a Webhook",{"path":2343,"title":2344,"kind":2320},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin","Add an Export to Excel Action in Django Admin",{"path":2346,"title":2347,"kind":2320},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-gradio-app-that-cleans-an-uploaded-excel-file","Build a Gradio App That Cleans an Uploaded Excel File",{"path":2349,"title":2350,"kind":2320},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-streamlit-app-to-upload-and-download-excel","Build a Streamlit App to Upload and Download Excel Files",{"path":2352,"title":2353,"kind":2320},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio","Build an Excel Workbook in Memory with BytesIO",1790972140484]