Productivity

Django inventory 速记

创建虚拟环境

python3 -m venv venv
source venv/bin/activate

安装 Django

pip install django mysqlclient

django-admin startproject ****

cd ******
python manage.py migrate # 创建初始数据库表
python manage.py createsuperuser # 创建管理员账号
python manage.py runserver 0.0.0.0:8000 # 启动开发服务器

pip install Pillow #用于显示图片

pip install djangorestframework #用于打开rest api接口

pip install django-import-export #用于导入导出excel

pip install openpyxl #用于导入导出excel

pip install django-extensions
pip install Werkzeug #用于https在dev模式下

当安装pip install mysqlclient失败的时候,你可以这样:

更新 apt

sudo apt update

安装编译工具和依赖

sudo apt install -y build-essential python3-dev default-libmysqlclient-dev libssl-dev pkg-config

然后再

pip install mysqlclient

默认左侧边栏缩进,使用js, 注意在***/admin.py里加入js位置

vi static/js/product_autocomplete.js
document.addEventListener('DOMContentLoaded', function() {
    const sidebar = document.getElementById('nav-sidebar');
    const toggleBtn = document.getElementById('toggle-nav-sidebar');

    if (sidebar && toggleBtn && sidebar.getAttribute('aria-expanded') === 'true') {
        // 触发折叠效果
        toggleBtn.click();
    }
});
vi ***/admin.py
    ...
    class Media:
        css = {
            'all': ('css/custom_admin.css',)  # 自定义 CSS 文件路径
            #'all': ("admin/css/admin_override.css",)
        }
        js = ("js/product_autocomplete.js",) # 自定义js
    ....

导入excel,前端按钮


 抽离 Excel 导入逻辑

# inventory/services/import_items.py

import pandas as pd
from inventory.models import Product


def parse_int(value, default=0):
    try:
        value_str = str(value).strip()
        num = int(''.join(c for c in value_str if c.isdigit()))
        return num
    except Exception:
        return default


def parse_decimal(value, default=0.0):
    try:
        value_str = str(value).strip()
        cleaned = ''.join(c for c in value_str if c.isdigit() or c == '.')
        return float(cleaned) if cleaned else default
    except Exception:
        return default


def import_products_from_excel(
    file_path,
    *,
    dry_run=False
):
    """
    从 Excel 导入 Product
    :param file_path: excel 文件路径
    :param dry_run: True 时不写入数据库
    :return: dict 导入结果
    """
    df = pd.read_excel(file_path)

    created = 0
    updated = 0
    errors = []

    for index, row in df.iterrows():
        try:
            defaults = {
                'manufacturer': row.get('manufacturer', ''),
                'lotno': row.get('lotno', ''),
                'package_type': row.get('package_type', ''),
                'stock_qty': parse_int(row.get('stock_qty', 0)),
                'price_sale': parse_decimal(row.get('price_sale', 0.0)),
                'remarks': row.get('remarks', ''),
                'location': row.get('location', ''),
            }

            if dry_run:
                continue

            obj, is_created = Product.objects.update_or_create(
                model=row['model'],
                defaults=defaults,
            )

            if is_created:
                created += 1
            else:
                updated += 1

        except Exception as e:
            errors.append({
                "row": index + 2,   # Excel 行号(含表头)
                "error": str(e),
            })

    return {
        "created": created,
        "updated": updated,
        "errors": errors,
        "total": len(df),
    }

vi inventory/management/commands/import_excel.py

# inventory/management/commands/import_excel.py

from django.core.management.base import BaseCommand
from inventory.services.import_items import import_products_from_excel


class Command(BaseCommand):
    help = 'Import inventory from Excel'

    def add_arguments(self, parser):
        parser.add_argument('file', type=str, help='Path to Excel file')
        parser.add_argument(
            '--dry-run',
            action='store_true',
            help='Parse excel but do not write to database',
        )

    def handle(self, *args, **options):
        result = import_products_from_excel(
            options['file'],
            dry_run=options['dry_run']
        )

        self.stdout.write(self.style.SUCCESS(
            f"Import finished! "
            f"Total={result['total']} "
            f"Created={result['created']} "
            f"Updated={result['updated']} "
            f"Errors={len(result['errors'])}"
        ))

        if result['errors']:
            self.stdout.write(self.style.WARNING("Errors:"))
            for err in result['errors'][:5]:
                self.stdout.write(str(err))


表单(Form)vi inventory/forms.py

# inventory/forms.py

from django import forms

class ExcelImportForm(forms.Form):
    file = forms.FileField(
        label="上传 Excel",
        help_text="仅支持 .xlsx"
    )


 View(上传 + 临时保存 + 导入)inventory/views.py

# inventory/views.py

import tempfile
from django.shortcuts import render, redirect
from inventory.forms import ExcelImportForm
from inventory.services.import_items import import_products_from_excel

def import_excel_view(request):
    if request.method == "POST":
        form = ExcelImportForm(request.POST, request.FILES)
        if form.is_valid():
            excel_file = form.cleaned_data["file"]

            with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
                for chunk in excel_file.chunks():
                    tmp.write(chunk)

                import_products_from_excel(tmp.name)

            return redirect("import_success")
    else:
        form = ExcelImportForm()

    return render(request, "inventory/import_excel.html", {"form": form})
# Create your views here.

写一个上传excel的html: vi inventory/templates/admin/import_excel.html

# inventory/templates/admin/import_excel.html
{% extends "admin/base_site.html" %}

{% block content %}
<h1>导入前,请把excel表的第一行的列名改成相关键值,列名-键值对照表如下</h1>
<p style="color:#00FF00;">型号:model; 厂商/品牌:brand; 批号:lotno; 封装:package_type;库存数量:stock_qty;售价:price_sale;成本价:price_cost;仓库位置:location;说明/备注:remarks;</p>
<p style="color:#00FF00;">供应商:supplier_name;产品名:name;sku编号:sku_code</p>

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="开始导入" class="default">
</form>

{% endblock %}

注册URL: vi
inventory/urls.py

from django.urls import path
from .views import import_excel_view

urlpatterns = [
    path("import-excel/", import_excel_view, name="import_excel"),
]

修改 

admin.py

# inventory/admin.py

from django.contrib import admin, messages
from django.urls import path
from django.shortcuts import render, redirect
import tempfile

from inventory.models import Product
from inventory.forms import ExcelImportForm
from inventory.services.import_items import import_products_from_excel
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ("model", "manufacturer", "stock_qty", "price_sale")
    search_fields = ("model", "manufacturer")

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path(
                "import-excel/",
                self.admin_site.admin_view(self.import_excel_view),
                name="inventory_product_import_excel",
            ),
        ]
        return custom_urls + urls
    @permission_required("inventory.add_item")
    def import_excel_view(self, request):
        if request.method == "POST":
            form = ExcelImportForm(request.POST, request.FILES)
            if form.is_valid():
                excel_file = form.cleaned_data["file"]

                with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
                    for chunk in excel_file.chunks():
                        tmp.write(chunk)

                result = import_products_from_excel(tmp.name)

                messages.success(
                    request,
                    f"导入完成:"
                    f"总数 {result['total']},"
                    f"新增 {result['created']},"
                    f"更新 {result['updated']},"
                    f"错误 {len(result['errors'])}"
                )

                return redirect("..")
        else:
            form = ExcelImportForm()

        context = {
            "form": form,
            "title": "导入 Excel",
        }

        return render(request, "admin/import_excel.html", context)

前端的按钮,结构如下:

templates/
└── admin/
    └── inventory/
        └── product/
            └── change_list.html   ✅ 新增

路径必需是:

admin / app_label / model_name / change_list.html


vi templates/admin/inventory/product/change_list.html

{% extends "admin/change_list.html" %}

{% block object-tools %}
    <ul class="object-tools">
        <li>
            <a href="import-excel/" class="addlink">
                导入 Excel
            </a>
        </li>
    </ul>

{{ block.super }}
{% endblock %}

George

Geek for fun.

https://jimy.fun

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top