ERP 微型系統建置

簡介

ERP代表企業資源規劃,是一個全面集成和管理企業所有業務流程和資源的軟件系統。 ERP系統通常由多個模塊組成,包括財務、購買、銷售、庫存、生產、人力資源和客戶關係管理等。

ERP系統可以幫助企業自動化許多業務流程,從而提高效率、降低成本並改善生產力。 ERP系統還可以讓企業更好地管理供應鏈、生產進度和銷售情況,從而更好地預測市場需求和優化企業運營。

不同的ERP系統有不同的特點和功能,企業可以根據自身的需求和預算選擇適合的ERP系統。 然而,實施ERP系統是一個複雜的過程,需要企業精心計劃和管理,以確保系統能夠順利運作並產生預期的效益。

Open Source ERP System

https://github.com/himool/Himool-ERP
本專案僅供學術交流使用

初步規劃

環境規劃

  • IDE

    • PyCharm Professional
    • Navicat Premium 16
    • Docker
  • 前端

    • Vue 2.6
    • 元件: Ant Design Vue 1.x
  • 後端

    • Python 3.9
    • Django 3.2
    • Django REST framework 3.12.4
  • 資料庫

    • MySQL 8.0

安裝環境

基本安裝

pip install -r requirements.txt

前端安裝

yarn

cd frontend
yarn config set ignore-engines true
yarn add @vue/cli-service
package.json

1
2
3
4
"devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "~4.5.0"
}

yarn add @vue/cli-plugin-babel
yarn install

npm

cd frontend
npm i @vue/cli-service
npm i @vue/cli-plugin-babel

MySQL

資料庫安裝

  1. Docker 下載 MySQL image

    1
    docker pull mysql

    https://hub.docker.com/_/mysql

  2. Docker 啟動容器

    1
    docker run --name sql2 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=Dev127336 -d mysql:8  

    run : docker 建立 container 並且執行的指令
    –name : 指定容器為 sql2
    -p 3306:3306 : 將容器的 3306 端口映射到主機的 3306 端口。
    -e MYSQL_ROOT_PASSWORD=Dev127336 : 初始化 root 用戶的密碼為 Dev127336。
    -d mysql:8 : 背景執行 MySQL 映像

  3. 登入mysql

    1
    mysql -u root -p
  4. 修改密碼

    1
    ALTER USER 'root'@'localhost' IDENTIFIED BY 'Dev127336';
  5. 添加遠端登入

    1
    2
    CREATE USER 'DevAuth'@'%' IDENTIFIED WITH mysql_native_password BY 'Dev127336';
    GRANT ALL PRIVILEGES ON *.* TO 'DevAuth'@'%';

https://ithelp.ithome.com.tw/articles/10272193

資料庫設定

  1. 資料庫連結

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    DATABASES = {
    # 方法一
    'default': {
    'ENGINE': 'django.db.backends.mysql', # 資料庫引擎
    'NAME': 'erp_db', # 資料庫名稱
    'USER': 'admin', # 資料庫登錄用戶名
    'PASSWORD': 'admin', # 密碼
    'HOST': '127.0.0.1', # 資料庫主機IP,如保持預設,則為127.0.0.1
    'PORT': 3306, # 資料庫埠號,如保持默認,則為3306
    }
    }
  2. 資料庫字串設定為urf8mb4
    tools/create_configs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # Database
    # https://docs.djangoproject.com/en/3.2/ref/settings/#databases

    BASE_DIR = Path(__file__).resolve().parent.parent
    DATABASES = {{
    'default': {{
    'ENGINE': 'django.db.backends.mysql',
    'HOST': '{host}',
    'PORT': '3306',
    'USER': '{user}',
    'PASSWORD': '{passowrd}',
    'NAME': '{database_name}',
    'OPTIONS': {{'charset': 'utf8mb4'}},
    }}
    }}
    1
    2
    3
    4
    set global character_set_server=utf8mb4;
    set global character_set_database=utf8mb4;
    set global character_set_client=utf8mb4;
    set global character_set_connection=utf8mb4;
  3. 建立資料庫

    1
    CREATE DATABASE erp_db;
  4. 轉移資料庫

  • 重新製作資料遷移
    • python manage.py makemigrations
  • 應用資料遷移
    • python manage.py migrate
  1. 建立使用者
  • python manage.py runscript create_user

本地端執行

yarn

yarn serve

npm

npm run serve

伺服器端執行

功能擴充

前端頁面新增

  1. menus.js

    1
    2
    3
    4
    5
    {
    key: '10', name: '工程管理', icon: 'team', submenus: [
    {key: '/engineering/engineer', name: '工程師'}
    ]
    }
  2. frontend/src/views

  • views -> engineering/engineer/index.vue
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    <template>
    <div>
    <a-card title="生產計畫詳情">
    <a-button slot="extra" type="primary" style="margin-right: 8px;" ghost v-print="'#printContent'">
    <a-icon type="printer" />列印</a-button
    >
    <a-button
    slot="extra"
    type="primary"
    ghost
    @click="
    () => {
    this.$router.go(-1);
    }
    "
    >
    <a-icon type="left" />返回</a-button
    >
    <section id="printContent">
    <a-spin :spinning="loading">
    <img id="barcode" style="float: right" />
    <a-descriptions bordered>
    <a-descriptions-item label="生產計畫單號">
    {{ item.number }}
    </a-descriptions-item>
    <a-descriptions-item label="銷售單號">
    {{ item.sales_order_number }}
    </a-descriptions-item>
    <a-descriptions-item label="狀態">
    {{ item.status_display }}
    </a-descriptions-item>
    </a-descriptions>
    </a-spin>
    </section>
    </a-card>
    </div>
    </template>

    <script>
    export default {
    data() {
    return {
    loading: false,
    item: {}
    };
    }
    };
    </script>
  1. frontend/src/components
  • components -> frontend/components/Engineering/Engineer.vue
  1. frontend/src/api
  • api -> frontend/api/engineering.js
  1. frontend/src/router
  • router -> frontend/router/index.js

    1
    import engineering from './engineering'
    1
    const routes = [index, user, account, manage, system, report, basicData, goods, purchasing, sale, warehouse, finance, production,engineering];
  • router -> frontend/router/engineering.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export default {
    path: '/engineering',
    name: 'engineering',
    component: () => import('@/layouts/BaseLayout'),
    redirect: '/engineering/engineer',
    children: [
    {
    path: 'engineer',
    meta: { title: '工程管理', permission: 'engineering' },
    component: () => import('@/views/engineering/engineer/index')
    }
    ],
    }
  1. frontend/src
  • frontend/src/permissions.js
    1
    2
    3
    4
    export let permissions = {
    ...
    'engineer':'工程管理'
    }
  1. scripts/init_permission
    1
    2
    3
    4
    5
    6
    {
    'name': '工程管理',
    'permissions': [
    {'name': '工程師', 'code': 'engineer'}
    ]
    }

後端建立API

  1. 根目錄建立app
    1
    python manage.py startapp musics
    將musics資料夾移動到apps資料夾
  • 後端建立的檔案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    - musics
    - migrations
    - __init__.py
    - __init__.py
    - admin.py
    - apps.py
    - models.py
    - tests.py
    - views.py
  • 專案的檔案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - message
    - migrations
    - __init__.py
    - __init__.py
    - admin.py
    - apps.py
    - filters.py
    - models.py
    - permissions.py
    - schemas.py
    - serializers.py
    - tests.py
    - urls.py
    - views.py
  1. 加入設定檔
    project/setting.py

添加

  • apps.musics
  • rest_framework
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'rest_framework',
'django_filters',
'drf_spectacular',
'django_extensions',
'debug_toolbar',
...
'apps.option',
'apps.manage',
'apps.musics'
]
  1. Model-Template-View
  • Model: 透過model引入資料庫,建立後台
    • models.py
  • View: 透過view的定義,傳送頁面與get/post
    • views.py
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      from extensions.common.schema import *
      from extensions.common.base import *
      from extensions.permissions import *
      from extensions.exceptions import *
      from extensions.viewsets import *

      from apps.flow.serializers import *
      from apps.flow.permissions import *
      from apps.flow.filters import *
      from apps.flow.schemas import *
      from apps.flow.models import *
  • Template: 頁面顯示資料
    • 顯示頁面
      • frontend/src/views
      • frontend/src/components
    • 路由控制
      • frontend/src/router
    • Django REST Framework (DRF)
      • frontend/src/api
  1. Model 介紹
  • class Meta
    在 Django 中,每個模型(Model)都可以包含一個名為 Meta 的內部類別(Inner Class),該類定義了一些元數據(Meta Data),這些元數據描述了模型本身的一些特性。

Meta 內部類通常被用來定義以下內容:

  • 資料表的名稱:使用 db_table 屬性可以自定義資料表的名稱,否則 Django 會根據模型名稱自動生成一個名稱。

  • 模型的排序方式:使用 ordering 屬性可以定義模型實例的預設排序方式。該屬性可以是一個單一的欄位名稱,也可以是多個欄位名稱的列表,以逗號分隔。

  • 模型的限制條件:使用 unique_together 屬性可以指定模型的多個欄位必須共同滿足唯一性限制。使用 constraints 屬性可以指定模型的其他限制條件,例如 CHECK 約束。

  • 模型的資料庫連接:使用 using 屬性可以指定模型使用的資料庫連接,如果有多個資料庫,可以為不同的模型指定不同的連接。

  • 模型的顯示名稱:使用 verbose_name 屬性可以為模型定義一個人類可讀的名稱,例如 “文章” 或 “使用者”。

  • 模型的複數顯示名稱:使用 verbose_name_plural 屬性可以為模型定義一個複數形式的顯示名稱,例如 “文章” 的複數形式為 “文章”。

總之,Meta 內部類提供了一種簡單而方便的方式來定義模型的元數據,這些元數據能夠影響模型的行為和外觀。

  • unique_together
    它允許您在定義模型時指定多個欄位的組合必須唯一,這樣就可以實現複合唯一性約束(Compound Unique Constraint)。

例如,假設您有一個模型名為 Person,該模型有兩個欄位 first_name 和 last_name,您希望這兩個欄位的組合必須唯一,這樣就可以使用 unique_together 選項來實現:

1
2
3
4
5
6
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)

class Meta:
unique_together = ('first_name', 'last_name')

當您使用 unique_together 定義了一個唯一性約束後,Django 會自動創建一個複合索引(Compound Index)來加速查詢。這個索引可以確保該欄位組合的唯一性,如果嘗試插入一個重複的組合,則會引發一個 IntegrityError 錯誤。

需要注意的是,unique_together 選項接受一個元組(Tuple)作為參數,其中每個元素是一個欄位名稱。您可以定義任意數量的欄位,但最少需要定義兩個欄位。此外,使用 unique_together 定義的唯一性約束只對 Django 級別有效,不會在資料庫中建立真正的唯一性約束。如果需要在資料庫層面實現唯一性約束,請考慮使用 unique=True 屬性或 unique constraint。

  1. 多出的檔案
  • filters.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from django_filters.rest_framework import FilterSet
    from django_filters.filters import *
    from apps.stock_transfer.models import *

    class StockTransferOrderFilter(FilterSet):
    start_date = DateFilter(field_name='create_time', lookup_expr='gte', label='開始日期')
    end_date = DateFilter(field_name='create_time', lookup_expr='lt', label='結束日期')

    class Meta:
    model = StockTransferOrder
    fields = ['number', 'out_warehouse', 'in_warehouse', 'handler', 'is_void', 'creator','start_date', 'end_date']
    __all__ = [
    'StockTransferOrderFilter',
    ]
  • permissions.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    from extensions.permissions import ModelPermission


    class InventoryWarningPermission(ModelPermission):
    code = 'inventory_warning'


    class ShelfLifeWarningPermission(ModelPermission):
    code = 'shelf_life_warning'


    class StockInReminderPermission(ModelPermission):
    code = 'stock_in_reminder'


    class StockOutReminderPermission(ModelPermission):
    code = 'stock_out_reminder'


    __all__ = [
    'InventoryWarningPermission', 'ShelfLifeWarningPermission',
    'StockInReminderPermission', 'StockOutReminderPermission',
    ]
  • schemas.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from extensions.serializers import *


    class CSRFTokenResponse(Serializer):
    token = CharField(label='權杖')


    class LoginRequest(Serializer):
    username = CharField(label='用戶名')
    password = CharField(label='密碼')

    __all__ = [
    'CSRFTokenResponse', 'LoginRequest',
    ]
  • serializers.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    from extensions.common.base import *
    from extensions.serializers import *
    from extensions.exceptions import *
    from apps.stock_in.models import *
    from apps.stock_out.models import *


    class StockInOrderReminderSerializer(BaseSerializer):
    """入庫任務提醒"""

    warehouse_number = CharField(source='warehouse.number', read_only=True, label='倉庫編號')
    warehouse_name = CharField(source='warehouse.name', read_only=True, label='倉庫名稱')

    class Meta:
    model = StockInOrder
    fields = ['id', 'number', 'warehouse', 'warehouse_number', 'warehouse_name',
    'total_quantity', 'remain_quantity']


    class StockOutOrderReminderSerializer(BaseSerializer):
    """出庫任務提醒"""

    warehouse_number = CharField(source='warehouse.number', read_only=True, label='倉庫編號')
    warehouse_name = CharField(source='warehouse.name', read_only=True, label='倉庫名稱')

    class Meta:
    model = StockOutOrder
    fields = ['id', 'number', 'warehouse', 'warehouse_number', 'warehouse_name',
    'total_quantity', 'remain_quantity']


    __all__ = [
    'StockInOrderReminderSerializer', 'StockOutOrderReminderSerializer',
    ]
  • urls.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from extensions.routers import *
    from apps.message.views import *


    router = BaseRouter()
    router.register('inventory_warnings', InventoryWarningViewSet, 'inventory_warning')
    router.register('stock_in_order_reminders', StockInOrderReminderViewSet, 'stock_in_order_reminder')
    router.register('stock_out_order_reminders', StockOutOrderReminderViewSet, 'stock_out_order_reminder')
    urlpatterns = router.urls

後續改善

  • 套件升級
  • 修改業務流程

前端

後端

  • Django
    • Python 3.11
    • Django 4.2.0
    • Python Flask

資料庫

  • MySQL 8.0