Odoo 主题开发指南:为 Odoo 17/18 构建自定义主题
Odoo 主题生态系统每年在 Odoo App Store 上产生超过 1200 万美元的收入,但只有不到 8% 的主题获得 5 星级评级。容易被遗忘的主题和畅销书之间的区别在于理解 Odoo 的渲染管道、QWeb 模板继承以及网站构建器的拖放架构。
本指南涵盖了 Odoo 主题开发的每一层 - 从项目脚手架到高级 SCSS 架构,从构建自定义片段到使您的主题与 Odoo 网站构建器无缝协作。
要点
- Odoo 主题是具有特定目录结构的标准模块 — 它们扩展
website模块并通过资产捆绑系统注册模板、SCSS 和 JavaScript。 - QWeb 是 Odoo 的服务器端模板引擎 — 它使用编译为 HTML 的 XML 指令(t-if、t-foreach、t-call、t-inherit)。了解 XPath 的模板继承对于自定义网站的任何部分都至关重要。
- SCSS 管道 通过 Odoo 的资产管理器进行编译 - 自定义变量覆盖 Bootstrap 5 默认值,并且
_variables.scss必须在框架之前加载。 - 网站构建器集成需要定义非技术用户可以通过拖放编辑器进行配置的代码片段选项、自定义构建块和动态内容块。
- Odoo 中的响应式设计 利用 Bootstrap 5 的网格系统和 Odoo 特定的实用程序 - 测试所有断点并考虑 RTL 语言进行国际部署。
1.主题模块结构
Odoo 主题遵循标准模块结构,并添加了特定于网站的内容:
theme_ecosire/
├── __init__.py
├── __manifest__.py
├── data/
│ ├── theme_data.xml # Default pages, menus
│ └── images.xml # Pre-loaded image library
├── static/
│ ├── description/
│ │ ├── icon.png # Module icon (128x128)
│ │ └── icon.svg # SVG version
│ ├── src/
│ │ ├── img/ # Theme images
│ │ ├── fonts/ # Custom fonts
│ │ ├── scss/
│ │ │ ├── _variables.scss # Bootstrap variable overrides
│ │ │ ├── _mixins.scss # Custom SCSS mixins
│ │ │ ├── _typography.scss # Font stacks, sizes
│ │ │ ├── _header.scss # Header styles
│ │ │ ├── _footer.scss # Footer styles
│ │ │ ├── _snippets.scss # Snippet-specific styles
│ │ │ └── primary.scss # Main entry point
│ │ ├── js/
│ │ │ ├── snippets/ # Snippet JS (animations, interactions)
│ │ │ └── theme.js # Theme-wide JS
│ │ └── xml/
│ │ └── snippets.xml # OWL snippet templates
│ └── img/ # Public images (backgrounds, icons)
├── views/
│ ├── assets.xml # Asset bundle registration
│ ├── layout.xml # Header/footer templates
│ ├── pages.xml # Pre-built page templates
│ └── snippets/
│ ├── options.xml # Snippet options (color pickers, etc.)
│ └── s_custom_block.xml # Custom snippet definitions
└── tests/
└── test_theme.py # Theme installation tests
主题清单
# __manifest__.py
{
'name': 'Theme Ecosire',
'description': 'Modern, performance-optimized theme for Odoo Website',
'category': 'Theme',
'version': '18.0.1.0.0',
'author': 'ECOSIRE Private Limited',
'website': 'https://ecosire.com',
'license': 'LGPL-3',
'depends': ['website'],
'data': [
'views/assets.xml',
'views/layout.xml',
'views/pages.xml',
'views/snippets/options.xml',
'views/snippets/s_custom_block.xml',
],
'assets': {
'web.assets_frontend': [
# Variables MUST come before Bootstrap
('prepend', 'theme_ecosire/static/src/scss/_variables.scss'),
'theme_ecosire/static/src/scss/primary.scss',
'theme_ecosire/static/src/js/theme.js',
],
'website.assets_wysiwyg': [
'theme_ecosire/static/src/js/snippets/*.js',
],
},
'images': [
'static/description/banner.png',
'static/description/theme_screenshot.jpg',
],
'snippet_lists': {
'website.snippets': [
('replace', 'theme_ecosire.s_custom_hero'),
],
},
'installable': True,
'application': False,
'auto_install': False,
}
关于 assets 键的重要说明: 在 Odoo 17+ 中,__manifest__.py 中的 assets 字典是注册静态文件的首选方式。使用 <template inherit_id="web.assets_frontend"> 的旧 views/assets.xml 方法仍然有效,但正在被逐步淘汰。使用两者以获得最大兼容性。
2. QWeb 模板深入研究
QWeb 是 Odoo 的基于 XML 的模板引擎。它处理以 t- 为前缀的指令并输出 HTML。了解 QWeb 对于主题开发来说是不容忽视的。
核心指令
<!-- Conditional rendering -->
<div t-if="website.is_publisher()">
<span>Edit mode controls here</span>
</div>
<div t-elif="request.env.user._is_internal()">
<span>Internal user view</span>
</div>
<div t-else="">
<span>Public visitor view</span>
</div>
<!-- Loops -->
<ul>
<t t-foreach="website.menu_id.child_id" t-as="menu_item">
<li t-attf-class="nav-item #{('active' if menu_item.is_active else '')}">
<a t-att-href="menu_item.url" t-out="menu_item.name"/>
</li>
</t>
</ul>
<!-- Variable setting -->
<t t-set="company" t-value="res_company"/>
<t t-set="primary_color" t-value="'#FF6B00'"/>
<!-- Output (auto-escaped) -->
<span t-out="partner.name"/>
<!-- Raw HTML output (use with caution) -->
<div t-raw="sanitized_html_content"/>
<!-- Dynamic attributes -->
<div t-att-id="'section-' + str(section.id)"
t-attf-class="container #{extra_class}"
t-att-data-section="section.sequence"/>
<!-- Template calling -->
<t t-call="theme_ecosire.hero_section">
<t t-set="title">Welcome to Our Store</t>
<t t-set="subtitle">Premium Odoo Themes</t>
</t>
使用 XPath 进行模板继承
这就是主题开发的强大之处。您可以修改任何现有模板而不替换它:
<!-- views/layout.xml -->
<template id="custom_header" inherit_id="website.layout" name="Custom Header">
<!-- Replace the entire header -->
<xpath expr="//header" position="replace">
<header id="top" class="ecosire-header">
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand" t-att-href="'/'">
<img t-if="website.logo"
t-att-src="website.image_url(website, 'logo')"
alt="Logo" class="img-fluid" style="max-height: 50px;"/>
</a>
<!-- Custom mega menu -->
<t t-call="theme_ecosire.mega_menu"/>
<!-- CTA button -->
<a href="/contactus" class="btn btn-primary ms-3">
Get Started
</a>
</div>
</nav>
</header>
</xpath>
<!-- Add a top bar before the header -->
<xpath expr="//header[@id='top']" position="before">
<div class="ecosire-topbar bg-dark text-white py-1">
<div class="container d-flex justify-content-between">
<span t-out="res_company.phone"/>
<span t-out="res_company.email"/>
</div>
</div>
</xpath>
<!-- Modify footer: add a newsletter section -->
<xpath expr="//footer" position="inside">
<div class="ecosire-newsletter bg-primary text-white py-5">
<div class="container text-center">
<h3>Stay Updated</h3>
<t t-call="website.s_newsletter_block"/>
</div>
</div>
</xpath>
</template>
XPath 位置参考
| 位置 | 效果 |
|---|---|
| 代码0 | 完全替换匹配的元素 |
| 代码0 | 追加到匹配的元素 |
| 代码0 | 在匹配元素 |
| 代码0 | 在匹配元素 |
| 代码0 | 修改匹配元素的属性 |
<!-- Modify attributes example -->
<xpath expr="//div[@id='wrapwrap']" position="attributes">
<attribute name="class" add="ecosire-theme" separator=" "/>
</xpath>
<!-- Remove an element -->
<xpath expr="//div[hasclass('o_not_editable')]" position="replace"/>
3.SCSS架构
Odoo 的 SCSS 管道通过其资源管理器进行编译,这意味着您的变量覆盖必须在 Bootstrap 的默认值之前加载。在清单的资产字典中使用“prepend”指令来确保您的 _variables.scss 文件具有优先权。这个文件控制颜色、字体、间距、边框半径以及主题中的所有其他 Bootstrap 设计标记。
变量覆盖 (_variables.scss)
// _variables.scss — loaded BEFORE Bootstrap
// Override Bootstrap 5 defaults
// Brand Colors
$o-brand-primary: #FF6B00;
$o-brand-secondary: #1A1F36;
// Bootstrap color overrides
$primary: $o-brand-primary;
$secondary: $o-brand-secondary;
$success: #00C853;
$info: #2196F3;
$warning: #FFB300;
$danger: #FF1744;
// Typography
$font-family-sans-serif: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
$font-family-monospace: 'JetBrains Mono', 'Fira Code', monospace;
$font-size-base: 1rem;
$headings-font-weight: 700;
$headings-line-height: 1.2;
// Spacing
$spacer: 1rem;
// Border radius
$border-radius: 0.5rem;
$border-radius-lg: 1rem;
$border-radius-pill: 50rem;
// Shadows
$box-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
$box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
$box-shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
// Navbar
$navbar-padding-y: 1rem;
$navbar-brand-font-size: 1.5rem;
// Cards
$card-border-radius: $border-radius-lg;
$card-border-color: transparent;
$card-box-shadow: $box-shadow;
主样式表 (primary.scss)
// primary.scss — Main entry point
@import 'variables'; // Already prepended, but import for IDE support
@import 'mixins';
@import 'typography';
@import 'header';
@import 'footer';
@import 'snippets';
// ============================================
// Global Overrides
// ============================================
#wrapwrap.ecosire-theme {
// Smooth scrolling
scroll-behavior: smooth;
// Better text rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// ============================================
// Section Spacing
// ============================================
.s_section {
padding: 5rem 0;
@media (max-width: 768px) {
padding: 3rem 0;
}
}
// ============================================
// Button Styles
// ============================================
.btn-primary {
background: linear-gradient(135deg, $primary, darken($primary, 10%));
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
letter-spacing: 0.02em;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba($primary, 0.4);
}
}
// ============================================
// Card Component
// ============================================
.ecosire-card {
background: white;
border-radius: $border-radius-lg;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: $box-shadow-lg;
}
&__image {
aspect-ratio: 16 / 9;
object-fit: cover;
width: 100%;
}
&__body {
padding: 1.5rem;
}
}
自定义字体加载
// _typography.scss
@font-face {
font-family: 'Inter';
src: url('/theme_ecosire/static/src/fonts/Inter-Variable.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Inter';
src: url('/theme_ecosire/static/src/fonts/Inter-Variable-Italic.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: italic;
}
// Heading hierarchy
h1, .h1 { font-size: 2.5rem; font-weight: 800; }
h2, .h2 { font-size: 2rem; font-weight: 700; }
h3, .h3 { font-size: 1.5rem; font-weight: 600; }
h4, .h4 { font-size: 1.25rem; font-weight: 600; }
4. 自定义片段(构建块)
片段是 Odoo 网站构建器中的拖放构建块。创建自定义片段使主题真正有价值。
定义片段模板
<!-- views/snippets/s_custom_block.xml -->
<odoo>
<!-- The snippet template -->
<template id="s_ecosire_hero" name="Ecosire Hero Section">
<section class="s_ecosire_hero pt-5 pb-5 o_cc o_cc1">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold">
Transform Your Business
</h1>
<p class="lead text-muted mt-3">
Enterprise solutions that scale with your ambitions.
</p>
<div class="mt-4">
<a href="/contactus" class="btn btn-primary btn-lg me-3">
Get Started
</a>
<a href="/about" class="btn btn-outline-secondary btn-lg">
Learn More
</a>
</div>
</div>
<div class="col-lg-6">
<img src="/theme_ecosire/static/src/img/hero-illustration.svg"
alt="Hero" class="img-fluid" loading="lazy"/>
</div>
</div>
</div>
</section>
</template>
<!-- Register snippet in the builder panel -->
<template id="snippets" inherit_id="website.snippets">
<xpath expr="//div[@id='snippet_structure']//t[@t-snippet]/.."
position="inside">
<t t-snippet="theme_ecosire.s_ecosire_hero"
t-thumbnail="/theme_ecosire/static/src/img/snippets/hero_thumb.png"/>
</xpath>
</template>
</odoo>
片段选项(用户自定义)
<!-- views/snippets/options.xml -->
<odoo>
<template id="s_ecosire_hero_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="EcosireHeroOptions"
data-selector=".s_ecosire_hero">
<!-- Layout selector -->
<we-select string="Layout">
<we-button data-select-class="s_hero_left">Text Left</we-button>
<we-button data-select-class="s_hero_center">Centered</we-button>
<we-button data-select-class="s_hero_right">Text Right</we-button>
</we-select>
<!-- Background style -->
<we-select string="Background">
<we-button data-select-class="bg-white">White</we-button>
<we-button data-select-class="bg-dark text-white">Dark</we-button>
<we-button data-select-class="bg-gradient">Gradient</we-button>
</we-select>
<!-- Animation toggle -->
<we-checkbox string="Enable Animation"
data-toggle-class="s_hero_animated"/>
</div>
</xpath>
</template>
</odoo>
JavaScript 片段(动画)
// static/src/js/snippets/s_ecosire_hero.js
/** @odoo-module */
import publicWidget from '@web/legacy/js/public/public_widget';
const EcosireHero = publicWidget.Widget.extend({
selector: '.s_ecosire_hero.s_hero_animated',
disabledInEditableMode: true,
start() {
this._super(...arguments);
this._initAnimation();
return Promise.resolve();
},
_initAnimation() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.2 }
);
this.el.querySelectorAll('.animate-target').forEach(el => {
observer.observe(el);
});
},
destroy() {
this._super(...arguments);
},
});
publicWidget.registry.EcosireHero = EcosireHero;
export default EcosireHero;
5.响应式设计最佳实践
移动优先 SCSS
// _responsive.scss
// Mobile-first breakpoints (Bootstrap 5)
// xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px
.ecosire-header {
// Mobile default
padding: 0.5rem 0;
.navbar-brand img {
max-height: 35px;
}
.nav-link {
padding: 0.75rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
// Tablet and up
@include media-breakpoint-up(md) {
padding: 0.75rem 0;
.nav-link {
padding: 0.5rem 1rem;
border-bottom: none;
}
}
// Desktop
@include media-breakpoint-up(lg) {
padding: 1rem 0;
.navbar-brand img {
max-height: 50px;
}
}
}
// Touch-friendly elements
@media (hover: none) {
.btn {
min-height: 44px;
min-width: 44px;
}
.nav-link {
min-height: 44px;
display: flex;
align-items: center;
}
}
RTL 支持
// _rtl.scss — Critical for Arabic, Hebrew, Urdu markets
html[dir="rtl"] {
.ecosire-header {
.navbar-brand {
margin-left: 1rem;
margin-right: 0;
}
.btn + .btn {
margin-right: 0.75rem;
margin-left: 0;
}
}
.ecosire-card__body {
text-align: right;
}
// Use logical properties where possible
.ms-3 { margin-inline-start: 1rem; }
.me-3 { margin-inline-end: 1rem; }
}
6. 性能优化
图像优化
<!-- Use responsive images with srcset -->
<img t-att-src="'/web/image/ir.attachment/%s/datas/800x400' % image.id"
t-att-srcset="'/web/image/ir.attachment/%s/datas/400x200 400w, /web/image/ir.attachment/%s/datas/800x400 800w, /web/image/ir.attachment/%s/datas/1200x600 1200w' % (image.id, image.id, image.id)"
sizes="(max-width: 576px) 400px, (max-width: 992px) 800px, 1200px"
loading="lazy"
decoding="async"
alt="Descriptive alt text"/>
关键 CSS 内联
<!-- views/layout.xml -->
<template id="critical_css" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<style type="text/css">
/* Inline critical above-the-fold CSS */
.ecosire-header { min-height: 72px; }
.s_ecosire_hero { min-height: 60vh; }
.navbar-brand img { max-height: 50px; }
</style>
</xpath>
</template>
资产加载策略
# __manifest__.py
'assets': {
# Critical CSS — loaded in head
'web.assets_frontend': [
('prepend', 'theme_ecosire/static/src/scss/_variables.scss'),
'theme_ecosire/static/src/scss/primary.scss',
],
# Non-critical JS — deferred
'web.assets_frontend_lazy': [
'theme_ecosire/static/src/js/animations.js',
'theme_ecosire/static/src/js/parallax.js',
],
# Editor-only assets
'website.assets_wysiwyg': [
'theme_ecosire/static/src/js/snippets/*.js',
'theme_ecosire/static/src/scss/_editor.scss',
],
},
7. 网站构建器集成
动态内容块
<!-- A snippet that fetches products dynamically -->
<template id="s_featured_products" name="Featured Products Grid">
<section class="s_featured_products pt-5 pb-5">
<div class="container">
<h2 class="text-center mb-5">Featured Products</h2>
<div class="row" t-if="products">
<t t-foreach="products[:8]" t-as="product">
<div class="col-md-3 mb-4">
<div class="ecosire-card h-100">
<img t-att-src="product.image_url"
t-att-alt="product.name"
class="ecosire-card__image" loading="lazy"/>
<div class="ecosire-card__body">
<h5 t-out="product.name"/>
<p class="text-primary fw-bold"
t-out="product.list_price"
t-options='{"widget": "monetary"}'/>
<a t-att-href="product.website_url"
class="btn btn-outline-primary btn-sm">
View Details
</a>
</div>
</div>
</div>
</t>
</div>
</div>
</section>
</template>
页面模板
<!-- Pre-built page templates that users can select -->
<template id="landing_page_template" name="Landing Page">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<t t-call="theme_ecosire.s_ecosire_hero"/>
<t t-call="theme_ecosire.s_features_grid"/>
<t t-call="theme_ecosire.s_testimonials"/>
<t t-call="theme_ecosire.s_cta_banner"/>
</div>
</t>
</template>
8. 测试你的主题
# tests/test_theme.py
from odoo.tests.common import HttpCase, tagged
@tagged('post_install', '-at_install')
class TestThemeEcosire(HttpCase):
def test_01_theme_installation(self):
"""Theme installs without errors."""
module = self.env['ir.module.module'].search([
('name', '=', 'theme_ecosire')
])
self.assertEqual(module.state, 'installed')
def test_02_homepage_loads(self):
"""Homepage renders with theme applied."""
response = self.url_open('/')
self.assertEqual(response.status_code, 200)
self.assertIn('ecosire-theme', response.text)
def test_03_snippets_available(self):
"""Custom snippets appear in the builder."""
self.start_tour('/web#action=website.action_website',
'theme_ecosire_snippet_tour', login='admin')
def test_04_responsive_meta(self):
"""Viewport meta tag is present."""
response = self.url_open('/')
self.assertIn('viewport', response.text)
9. 在 Odoo App Store 上发布
| 要求 | 详情 |
|---|---|
| 截图 | 3-5张高质量截图(1920x1080) |
| 图标 | static/description/ 中的 128x128 PNG |
| 描述 | static/description/index.html 中的 HTML 描述 |
| 许可证 | LGPL-3 主题(标准实践) |
| 版本 | 匹配 Odoo 系列 (18.0.x.y.z) |
| 测试 | 通过 Odoo 的自动 linting 检查 |
| 演示数据 | 包括先试后买的演示数据 |
常见问题
我可以在 Odoo 主题中使用 Tailwind CSS 吗?
虽然技术上可行,但将 Tailwind CSS 与 Odoo 的内置 Bootstrap 5 一起使用会产生冲突并显着增加包大小。推荐的方法是使用 Bootstrap 5 实用程序和 SCSS 自定义属性,它们与 Odoo 的资产管道和网站构建器本机集成。如果您需要 Bootstrap 未提供的实用程序类,请创建自定义 SCSS 实用程序。
如何使我的主题与 Odoo 17 和 18 兼容?
为每个 Odoo 版本创建单独的分支。 Odoo 17 使用 Bootstrap 5.1,而 Odoo 18 使用带有 CSS 变量支持的 Bootstrap 5.3。资产注册也发生了变化 - Odoo 18 更喜欢清单资产密钥,而 Odoo 17 使用 XML 模板继承。维护共享的 SCSS 部分和特定于版本的集成文件。
为什么我的 SCSS 变量覆盖不起作用?
最常见的原因是加载顺序。 _variables.scss 必须在 Bootstrap 编译之前加载。在清单中使用“prepend”指令:('prepend', 'theme_name/static/src/scss/_variables.scss')。还要确保您覆盖正确的变量名称 - Odoo 使用 o- 前缀版本(例如 $o-brand-primary)包装一些 Bootstrap 变量。
自定义代码段如何与网站构建器配合使用?
自定义片段是通过 website.snippets 的模板继承在片段面板中注册的 QWeb 模板。每个片段都需要一个 HTML 结构模板、在 website.snippet_options 中定义的自定义面板可选选项以及扩展 publicWidget.Widget 的可选 JavaScript 以实现交互性。网站构建器自动处理注册片段的拖放、内联编辑和撤消/重做。
向 Odoo 主题添加自定义字体的最佳方式是什么?
将 WOFF2 字体文件放置在 static/src/fonts/ 中,并在 SCSS 中定义 @font-face 规则。使用 font-display: swap 来提高性能。然后覆盖 _variables.scss 中的 $font-family-sans-serif Bootstrap 变量。避免从外部 CDN 加载字体,因为它会添加 DNS 查找,并且在受限网络中可能会失败。对字体进行子集化,使其仅包含所需的字符范围。
如何在 Odoo 主题中支持深色模式?
Odoo 18 通过 CSS 自定义属性引入了原生深色模式支持。使用 [data-bs-theme="dark"] 选择器覆盖主题中的暗模式变量。定义自定义颜色、背景和阴影的浅色和深色变体。网站构建器切换可处理切换 - 您的主题只需为两种模式提供正确的 CSS 变量值。
后续步骤
主题开发是设计与工程的结合。精心构建的 Odoo 主题可以为数百名客户提供服务,并在 Odoo 应用商店上产生可观的经常性收入。
相关指南:
- Odoo Python 开发指南 — 后端开发基础
- Odoo 自定义模块开发 — 模块架构概述
- Odoo REST API 教程 — API 集成模式
需要自定义 Odoo 主题或重新设计网站? ECOSIRE 的 Odoo 定制团队 通过完整的网站构建器集成、跨所有断点的响应式设计以及对国际部署的 RTL 支持来设计和构建企业级主题。我们已为 40 多个国家/地区的客户提供定制主题。 联系我们获取免费设计咨询。
作者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
相关文章
AI 支持的客户细分:从 RFM 到预测聚类
了解 AI 如何将客户细分从静态 RFM 分析转变为动态预测聚类。使用 Python、Odoo 和真实 ROI 数据的实施指南。
用于供应链优化的人工智能:可见性、预测和自动化
利用人工智能改变供应链运营:需求感知、供应商风险评分、路线优化、仓库自动化和中断预测。 2026年指南。
B2B电子商务战略:2026年打造在线批发业务
通过批发定价、帐户管理、信用条款、打孔目录和 Odoo B2B 门户配置策略来掌握 B2B 电子商务。