create-odoo-module 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/bin/create-odoo-module.js +147 -0
- package/package.json +84 -0
- package/src/cli/args-parser.js +36 -0
- package/src/cli/index.js +272 -0
- package/src/cli/prompts.js +151 -0
- package/src/cli/validator.js +72 -0
- package/src/config/defaults.js +41 -0
- package/src/config/pro-config.js +55 -0
- package/src/generators/api-generator.js +340 -0
- package/src/generators/deploy-generator.js +390 -0
- package/src/generators/flutter-generator.js +1695 -0
- package/src/generators/odoo-generator.js +794 -0
- package/src/generators/ui-generator.js +329 -0
- package/src/utils/file-system.js +67 -0
- package/src/utils/license-check.js +57 -0
- package/src/utils/logger.js +32 -0
- package/src/utils/spinner.js +32 -0
- package/src/utils/string-utils.js +65 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const boxen = require('boxen');
|
|
8
|
+
const figures = require('figures');
|
|
9
|
+
|
|
10
|
+
const { askInteractiveQuestions } = require('./prompts');
|
|
11
|
+
const { validateModuleName } = require('./validator');
|
|
12
|
+
const { parseExtraDepends } = require('./args-parser');
|
|
13
|
+
const { verifyProLicense } = require('../utils/license-check');
|
|
14
|
+
const { generateOdooModule } = require('../generators/odoo-generator');
|
|
15
|
+
const { generateFlutterApp } = require('../generators/flutter-generator');
|
|
16
|
+
const { generateApiLayer } = require('../generators/api-generator');
|
|
17
|
+
const { generateUiLayer } = require('../generators/ui-generator');
|
|
18
|
+
const { generateDeployScripts } = require('../generators/deploy-generator');
|
|
19
|
+
const { toSnakeCase, toPascalCase, toKebabCase } = require('../utils/string-utils');
|
|
20
|
+
const logger = require('../utils/logger');
|
|
21
|
+
|
|
22
|
+
// ─── List templates ───────────────────────────────────────────────────────────
|
|
23
|
+
function listTemplates() {
|
|
24
|
+
const templates = [
|
|
25
|
+
{ name: 'fleet', label: 'Fleet Management', free: false, desc: 'Vehicle tracking, maintenance, fuel logs' },
|
|
26
|
+
{ name: 'hr', label: 'Human Resources', free: false, desc: 'Employees, contracts, payroll, leaves' },
|
|
27
|
+
{ name: 'inventory', label: 'Inventory', free: false, desc: 'Products, lots, barcode scanner, moves' },
|
|
28
|
+
{ name: 'pos', label: 'Point of Sale', free: false, desc: 'POS extension with custom payment methods' },
|
|
29
|
+
{ name: 'crm', label: 'CRM Pipeline', free: false, desc: 'Leads, pipeline, activities, dashboard' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
console.log(chalk.bold('\n Available Templates:\n'));
|
|
33
|
+
templates.forEach(t => {
|
|
34
|
+
const badge = t.free ? chalk.green('[FREE]') : chalk.yellow('[PRO] ');
|
|
35
|
+
console.log(` ${badge} ${chalk.cyan(t.name.padEnd(12))} ${chalk.bold(t.label)}`);
|
|
36
|
+
console.log(` ${chalk.dim(t.desc)}\n`);
|
|
37
|
+
});
|
|
38
|
+
console.log(chalk.dim(' Get Pro access → https://create-odoo-module.dev/pro\n'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Main CLI runner ──────────────────────────────────────────────────────────
|
|
42
|
+
async function runCLI(moduleName, options) {
|
|
43
|
+
logger.setVerbose(!!options.verbose);
|
|
44
|
+
|
|
45
|
+
// ── 1. Interactive mode when no module name given ─────────────────────────
|
|
46
|
+
if (!moduleName) {
|
|
47
|
+
if (options.interactive === false) {
|
|
48
|
+
logger.error('Module name is required when using --no-interactive');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const answers = await askInteractiveQuestions(null, options);
|
|
52
|
+
moduleName = answers.moduleName;
|
|
53
|
+
options = { ...options, ...answers };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── 2. Validate module name ───────────────────────────────────────────────
|
|
57
|
+
const validation = validateModuleName(moduleName);
|
|
58
|
+
if (!validation.valid) {
|
|
59
|
+
logger.error(`Invalid module name: ${validation.error}`);
|
|
60
|
+
logger.info('Module names must be lowercase with hyphens or underscores (e.g. fleet-manager)');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 3. Gather remaining options interactively if needed ───────────────────
|
|
65
|
+
let config = buildConfig(moduleName, options);
|
|
66
|
+
|
|
67
|
+
if (options.interactive !== false && !hasMinimalFlags(options)) {
|
|
68
|
+
const answers = await askInteractiveQuestions(moduleName, options);
|
|
69
|
+
config = buildConfig(moduleName, { ...options, ...answers });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── 4. Pro license verification ───────────────────────────────────────────
|
|
73
|
+
if (config.proKey || config.template) {
|
|
74
|
+
logger.verbose('Verifying Pro license...');
|
|
75
|
+
const isValid = await verifyProLicense(config.proKey);
|
|
76
|
+
if (!isValid && config.template) {
|
|
77
|
+
logger.warn(`Template "${config.template}" requires a Pro license.`);
|
|
78
|
+
logger.info('Get Pro access → https://create-odoo-module.dev/pro');
|
|
79
|
+
config.template = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 5. Target directory ───────────────────────────────────────────────────
|
|
84
|
+
const targetDir = path.resolve(process.cwd(), toKebabCase(moduleName));
|
|
85
|
+
|
|
86
|
+
if (await fs.pathExists(targetDir)) {
|
|
87
|
+
const { default: inquirer } = await import('inquirer');
|
|
88
|
+
const { overwrite } = await inquirer.prompt([{
|
|
89
|
+
type: 'confirm',
|
|
90
|
+
name: 'overwrite',
|
|
91
|
+
message: chalk.yellow(`Directory ${chalk.bold(toKebabCase(moduleName))} already exists. Overwrite?`),
|
|
92
|
+
default: false,
|
|
93
|
+
}]);
|
|
94
|
+
if (!overwrite) {
|
|
95
|
+
logger.info('Aborted.');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
await fs.remove(targetDir);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 6. Start generation ───────────────────────────────────────────────────
|
|
102
|
+
console.log();
|
|
103
|
+
logger.info(`Creating Odoo module: ${chalk.bold.cyan(config.moduleName)}`);
|
|
104
|
+
logger.info(`Target: ${chalk.dim(targetDir)}`);
|
|
105
|
+
console.log();
|
|
106
|
+
|
|
107
|
+
const steps = buildSteps(config);
|
|
108
|
+
let stepIndex = 0;
|
|
109
|
+
|
|
110
|
+
const spinner = ora({
|
|
111
|
+
spinner: 'dots2',
|
|
112
|
+
color: 'magenta',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Step: Odoo module
|
|
117
|
+
spinner.start(chalk.bold('Generating Odoo module...'));
|
|
118
|
+
await generateOdooModule(targetDir, config);
|
|
119
|
+
spinner.succeed(chalk.green(`${figures.tick} Odoo module`) + chalk.dim(` → odoo_module/`));
|
|
120
|
+
|
|
121
|
+
// Step: REST API
|
|
122
|
+
if (config.withApi) {
|
|
123
|
+
spinner.start(chalk.bold('Generating REST API controllers...'));
|
|
124
|
+
await generateApiLayer(targetDir, config);
|
|
125
|
+
spinner.succeed(chalk.green(`${figures.tick} REST API layer`) + chalk.dim(` → odoo_module/controllers/`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step: XML Views & OWL
|
|
129
|
+
spinner.start(chalk.bold('Generating Odoo views...'));
|
|
130
|
+
await generateUiLayer(targetDir, config);
|
|
131
|
+
spinner.succeed(chalk.green(`${figures.tick} Odoo views`) + chalk.dim(` → odoo_module/views/`));
|
|
132
|
+
|
|
133
|
+
// Step: Flutter app
|
|
134
|
+
if (config.withUi) {
|
|
135
|
+
spinner.start(chalk.bold('Generating Flutter application...'));
|
|
136
|
+
await generateFlutterApp(targetDir, config);
|
|
137
|
+
spinner.succeed(chalk.green(`${figures.tick} Flutter app`) + chalk.dim(` → flutter_app/`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Step: Deploy scripts
|
|
141
|
+
spinner.start(chalk.bold('Generating deploy scripts...'));
|
|
142
|
+
await generateDeployScripts(targetDir, config);
|
|
143
|
+
spinner.succeed(chalk.green(`${figures.tick} Deploy scripts`) + chalk.dim(` → scripts/`));
|
|
144
|
+
|
|
145
|
+
// Step: Git init
|
|
146
|
+
if (options.git !== false) {
|
|
147
|
+
spinner.start(chalk.bold('Initializing git repository...'));
|
|
148
|
+
await initGitRepo(targetDir);
|
|
149
|
+
spinner.succeed(chalk.green(`${figures.tick} Git initialized`));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log();
|
|
153
|
+
printSuccessMessage(config, targetDir);
|
|
154
|
+
|
|
155
|
+
} catch (err) {
|
|
156
|
+
spinner.fail(chalk.red(`Generation failed: ${err.message}`));
|
|
157
|
+
logger.verbose(err.stack);
|
|
158
|
+
await fs.remove(targetDir).catch(() => {});
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Build config object ──────────────────────────────────────────────────────
|
|
164
|
+
function buildConfig(moduleName, options) {
|
|
165
|
+
const snake = toSnakeCase(moduleName);
|
|
166
|
+
const pascal = toPascalCase(moduleName);
|
|
167
|
+
const kebab = toKebabCase(moduleName);
|
|
168
|
+
const odooModel = snake.replace(/_/g, '.');
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
// Names
|
|
172
|
+
moduleName,
|
|
173
|
+
moduleNameSnake: snake,
|
|
174
|
+
moduleNamePascal: pascal,
|
|
175
|
+
moduleNameKebab: kebab,
|
|
176
|
+
moduleNameOdoo: odooModel, // e.g. fleet.manager
|
|
177
|
+
moduleNameClass: pascal, // e.g. FleetManager
|
|
178
|
+
moduleNameLabel: pascal.replace(/([A-Z])/g, ' $1').trim(), // Fleet Manager
|
|
179
|
+
|
|
180
|
+
// Odoo config
|
|
181
|
+
odooVersion: options.odooVersion || '17',
|
|
182
|
+
category: options.category || 'Custom',
|
|
183
|
+
author: options.author || 'Your Company',
|
|
184
|
+
website: options.website || 'https://yourcompany.com',
|
|
185
|
+
extraDepends: parseExtraDepends(options.depends || ''),
|
|
186
|
+
|
|
187
|
+
// Feature flags
|
|
188
|
+
withApi: !!options.withApi,
|
|
189
|
+
withUi: !!options.withUi,
|
|
190
|
+
withTests: !!options.withTests,
|
|
191
|
+
withReports: !!options.withReports,
|
|
192
|
+
withOwl: !!options.withOwl,
|
|
193
|
+
withWizard: !!options.withWizard,
|
|
194
|
+
withDocker: options.withDocker !== false,
|
|
195
|
+
withCi: !!options.withCi,
|
|
196
|
+
|
|
197
|
+
// Pro
|
|
198
|
+
proKey: options.proKey || null,
|
|
199
|
+
template: options.template || null,
|
|
200
|
+
|
|
201
|
+
// Meta
|
|
202
|
+
year: new Date().getFullYear(),
|
|
203
|
+
generatedAt: new Date().toISOString(),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function hasMinimalFlags(options) {
|
|
208
|
+
return options.withApi || options.withUi || options.withTests ||
|
|
209
|
+
options.template || options.interactive === false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildSteps(config) {
|
|
213
|
+
const steps = ['Odoo Module', 'XML Views'];
|
|
214
|
+
if (config.withApi) steps.push('REST API');
|
|
215
|
+
if (config.withUi) steps.push('Flutter App');
|
|
216
|
+
if (config.withReports) steps.push('QWeb Reports');
|
|
217
|
+
steps.push('Deploy Scripts', 'Git Init');
|
|
218
|
+
return steps;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Git init ─────────────────────────────────────────────────────────────────
|
|
222
|
+
async function initGitRepo(dir) {
|
|
223
|
+
const { execa } = require('execa');
|
|
224
|
+
try {
|
|
225
|
+
await execa('git', ['init'], { cwd: dir });
|
|
226
|
+
await execa('git', ['add', '-A'], { cwd: dir });
|
|
227
|
+
await execa('git', ['commit', '--allow-empty', '-m', 'chore: initial commit from create-odoo-module'], { cwd: dir });
|
|
228
|
+
} catch (_) {
|
|
229
|
+
// git may not be installed — not fatal
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Success message ──────────────────────────────────────────────────────────
|
|
234
|
+
function printSuccessMessage(config, targetDir) {
|
|
235
|
+
const dirName = path.basename(targetDir);
|
|
236
|
+
|
|
237
|
+
const lines = [
|
|
238
|
+
chalk.green.bold(`${figures.tick} Your Odoo project is ready!`),
|
|
239
|
+
'',
|
|
240
|
+
chalk.bold('Get started:'),
|
|
241
|
+
` ${chalk.cyan(`cd ${dirName}`)}`,
|
|
242
|
+
` ${chalk.cyan('cp .env.example .env')} ${chalk.dim('← add your credentials')}`,
|
|
243
|
+
config.withDocker
|
|
244
|
+
? ` ${chalk.cyan('npm run dev')} ${chalk.dim('← start local Odoo via Docker')}`
|
|
245
|
+
: '',
|
|
246
|
+
` ${chalk.cyan('npm run deploy')} ${chalk.dim('← upload module to Odoo')}`,
|
|
247
|
+
config.withUi
|
|
248
|
+
? ` ${chalk.cyan('npm run flutter:run')} ${chalk.dim('← start Flutter app')}`
|
|
249
|
+
: '',
|
|
250
|
+
'',
|
|
251
|
+
chalk.bold('What was generated:'),
|
|
252
|
+
` ${chalk.yellow('odoo_module/')} Full Odoo Python module`,
|
|
253
|
+
config.withApi ? ` ${chalk.yellow('controllers/')} REST API (GET/POST/PUT/DELETE)` : '',
|
|
254
|
+
config.withUi ? ` ${chalk.yellow('flutter_app/')} Production-ready Flutter app` : '',
|
|
255
|
+
config.withReports ? ` ${chalk.yellow('report/')} QWeb PDF reports` : '',
|
|
256
|
+
` ${chalk.yellow('scripts/')} Deploy + Docker Compose`,
|
|
257
|
+
'',
|
|
258
|
+
`${chalk.dim('Docs:')} https://create-odoo-module.dev/docs`,
|
|
259
|
+
`${chalk.dim('Pro:')} https://create-odoo-module.dev/pro`,
|
|
260
|
+
].filter(l => l !== '');
|
|
261
|
+
|
|
262
|
+
console.log(
|
|
263
|
+
boxen(lines.join('\n'), {
|
|
264
|
+
padding: 1,
|
|
265
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
266
|
+
borderStyle: 'round',
|
|
267
|
+
borderColor: 'green',
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { runCLI, listTemplates };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interactive CLI prompts using Inquirer.js
|
|
7
|
+
* Runs when user calls `npx create-odoo-module` without sufficient flags.
|
|
8
|
+
*/
|
|
9
|
+
async function askInteractiveQuestions(moduleName, existingOptions = {}) {
|
|
10
|
+
// Inquirer v9+ is ESM — dynamic import required
|
|
11
|
+
const { default: inquirer } = await import('inquirer');
|
|
12
|
+
|
|
13
|
+
const questions = [];
|
|
14
|
+
|
|
15
|
+
// ── Module name ────────────────────────────────────────────────────────────
|
|
16
|
+
if (!moduleName) {
|
|
17
|
+
questions.push({
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'moduleName',
|
|
20
|
+
message: chalk.cyan('Module name') + chalk.dim(' (e.g. fleet-manager):'),
|
|
21
|
+
validate: (input) => {
|
|
22
|
+
if (!input.trim()) return 'Module name is required';
|
|
23
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(input.trim())) {
|
|
24
|
+
return 'Use lowercase letters, numbers, hyphens or underscores only';
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
filter: (input) => input.trim().toLowerCase(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Odoo version ───────────────────────────────────────────────────────────
|
|
33
|
+
questions.push({
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'odooVersion',
|
|
36
|
+
message: chalk.cyan('Odoo version:'),
|
|
37
|
+
choices: [
|
|
38
|
+
{ name: '18.0 (latest)', value: '18' },
|
|
39
|
+
{ name: '17.0 (stable) ← recommended', value: '17' },
|
|
40
|
+
{ name: '16.0 (LTS)', value: '16' },
|
|
41
|
+
],
|
|
42
|
+
default: '17',
|
|
43
|
+
when: !existingOptions.odooVersion,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── Features to include ────────────────────────────────────────────────────
|
|
47
|
+
questions.push({
|
|
48
|
+
type: 'checkbox',
|
|
49
|
+
name: 'features',
|
|
50
|
+
message: chalk.cyan('Select features to include:'),
|
|
51
|
+
choices: [
|
|
52
|
+
{
|
|
53
|
+
name: `${chalk.bold('REST API')} ${chalk.dim('GET/POST/PUT/DELETE endpoints')}`,
|
|
54
|
+
value: 'withApi',
|
|
55
|
+
checked: false,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: `${chalk.bold('Flutter App')} ${chalk.dim('Mobile app with login + CRUD')}`,
|
|
59
|
+
value: 'withUi',
|
|
60
|
+
checked: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: `${chalk.bold('Unit Tests')} ${chalk.dim('Python pytest + Dart flutter_test')}`,
|
|
64
|
+
value: 'withTests',
|
|
65
|
+
checked: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: `${chalk.bold('QWeb Reports')} ${chalk.dim('PDF report template')}`,
|
|
69
|
+
value: 'withReports',
|
|
70
|
+
checked: false,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: `${chalk.bold('OWL Components')} ${chalk.dim('JavaScript UI widgets')}`,
|
|
74
|
+
value: 'withOwl',
|
|
75
|
+
checked: false,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: `${chalk.bold('Wizard')} ${chalk.dim('Transient model dialog')}`,
|
|
79
|
+
value: 'withWizard',
|
|
80
|
+
checked: false,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: `${chalk.bold('Docker Compose')} ${chalk.dim('Local Odoo dev environment')}`,
|
|
84
|
+
value: 'withDocker',
|
|
85
|
+
checked: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: `${chalk.bold('GitHub Actions CI')} ${chalk.dim('Automated testing pipeline')}`,
|
|
89
|
+
value: 'withCi',
|
|
90
|
+
checked: false,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── Author info ────────────────────────────────────────────────────────────
|
|
96
|
+
questions.push(
|
|
97
|
+
{
|
|
98
|
+
type: 'input',
|
|
99
|
+
name: 'author',
|
|
100
|
+
message: chalk.cyan('Author / Company name:'),
|
|
101
|
+
default: existingOptions.author || 'Your Company',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'category',
|
|
106
|
+
message: chalk.cyan('Odoo app category:'),
|
|
107
|
+
default: existingOptions.category || 'Custom',
|
|
108
|
+
choices: ['Custom', 'Accounting', 'CRM', 'Human Resources', 'Inventory', 'Manufacturing', 'Sales', 'Website'],
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ── Server URL (for .env pre-fill) ────────────────────────────────────────
|
|
113
|
+
questions.push({
|
|
114
|
+
type: 'input',
|
|
115
|
+
name: 'odooUrl',
|
|
116
|
+
message: chalk.cyan('Odoo server URL') + chalk.dim(' (for .env):'),
|
|
117
|
+
default: 'http://localhost:8069',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
questions.push({
|
|
121
|
+
type: 'input',
|
|
122
|
+
name: 'odooDb',
|
|
123
|
+
message: chalk.cyan('Odoo database name') + chalk.dim(' (for .env):'),
|
|
124
|
+
default: 'odoo',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
128
|
+
const answers = await inquirer.prompt(questions);
|
|
129
|
+
|
|
130
|
+
// Flatten features array into boolean flags
|
|
131
|
+
const features = answers.features || [];
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
moduleName: answers.moduleName || moduleName,
|
|
135
|
+
odooVersion: answers.odooVersion || existingOptions.odooVersion || '17',
|
|
136
|
+
author: answers.author || existingOptions.author,
|
|
137
|
+
category: answers.category || existingOptions.category,
|
|
138
|
+
odooUrl: answers.odooUrl || 'http://localhost:8069',
|
|
139
|
+
odooDb: answers.odooDb || 'odoo',
|
|
140
|
+
withApi: features.includes('withApi') || !!existingOptions.withApi,
|
|
141
|
+
withUi: features.includes('withUi') || !!existingOptions.withUi,
|
|
142
|
+
withTests: features.includes('withTests') || !!existingOptions.withTests,
|
|
143
|
+
withReports: features.includes('withReports') || !!existingOptions.withReports,
|
|
144
|
+
withOwl: features.includes('withOwl') || !!existingOptions.withOwl,
|
|
145
|
+
withWizard: features.includes('withWizard') || !!existingOptions.withWizard,
|
|
146
|
+
withDocker: features.includes('withDocker') !== false,
|
|
147
|
+
withCi: features.includes('withCi') || !!existingOptions.withCi,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { askInteractiveQuestions };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const RESERVED_NAMES = new Set([
|
|
4
|
+
'base', 'web', 'mail', 'portal', 'website', 'sale', 'purchase',
|
|
5
|
+
'account', 'stock', 'hr', 'crm', 'project', 'mrp', 'point_of_sale',
|
|
6
|
+
'fleet', 'maintenance', 'helpdesk', 'sign', 'survey', 'timesheet',
|
|
7
|
+
'node_modules', 'dist', 'build', 'src', 'test', 'tests', 'public',
|
|
8
|
+
'static', 'assets', 'config', 'setup', 'install', 'odoo', 'openerp',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const MAX_LENGTH = 64;
|
|
12
|
+
const MIN_LENGTH = 2;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate an Odoo module name.
|
|
16
|
+
* Rules:
|
|
17
|
+
* - Lowercase letters, digits, hyphens, underscores only
|
|
18
|
+
* - Must start with a letter
|
|
19
|
+
* - Length between 2 and 64 chars
|
|
20
|
+
* - Not a reserved Odoo core module name
|
|
21
|
+
*
|
|
22
|
+
* @param {string} name
|
|
23
|
+
* @returns {{ valid: boolean, error?: string, warnings?: string[] }}
|
|
24
|
+
*/
|
|
25
|
+
function validateModuleName(name) {
|
|
26
|
+
if (typeof name !== 'string' || !name) {
|
|
27
|
+
return { valid: false, error: 'Module name must be a non-empty string' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trimmed = name.trim();
|
|
31
|
+
|
|
32
|
+
if (trimmed.length < MIN_LENGTH) {
|
|
33
|
+
return { valid: false, error: `Module name must be at least ${MIN_LENGTH} characters` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (trimmed.length > MAX_LENGTH) {
|
|
37
|
+
return { valid: false, error: `Module name must be at most ${MAX_LENGTH} characters` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!/^[a-z]/.test(trimmed)) {
|
|
41
|
+
return { valid: false, error: 'Module name must start with a lowercase letter' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(trimmed)) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: 'Module name may only contain lowercase letters, digits, hyphens (-) and underscores (_)',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const snakeName = trimmed.replace(/-/g, '_');
|
|
52
|
+
if (RESERVED_NAMES.has(snakeName) || RESERVED_NAMES.has(trimmed)) {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
error: `"${trimmed}" is a reserved Odoo module name. Choose a more specific name (e.g. custom-fleet-manager)`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const warnings = [];
|
|
60
|
+
|
|
61
|
+
if (trimmed.startsWith('custom_') || trimmed.startsWith('custom-')) {
|
|
62
|
+
warnings.push('Prefix "custom_" is redundant — consider removing it');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (trimmed.includes('--') || trimmed.includes('__')) {
|
|
66
|
+
warnings.push('Consecutive separators (-- or __) may cause issues');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: true, name: trimmed, warnings };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { validateModuleName };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default configuration values used throughout the CLI.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
odooVersion: '17',
|
|
8
|
+
author: 'Your Company',
|
|
9
|
+
website: 'https://yourcompany.com',
|
|
10
|
+
category: 'Custom',
|
|
11
|
+
license: 'LGPL-3',
|
|
12
|
+
withApi: false,
|
|
13
|
+
withUi: false,
|
|
14
|
+
withTests: false,
|
|
15
|
+
withReports: false,
|
|
16
|
+
withOwl: false,
|
|
17
|
+
withWizard: false,
|
|
18
|
+
withDocker: true,
|
|
19
|
+
withCi: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Core Odoo module depends — always included.
|
|
24
|
+
*/
|
|
25
|
+
const CORE_DEPENDS = ['base', 'mail', 'web'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Odoo version → compatibility_date mapping.
|
|
29
|
+
*/
|
|
30
|
+
const COMPATIBILITY_DATES = {
|
|
31
|
+
'16': '2023-01-01',
|
|
32
|
+
'17': '2024-01-01',
|
|
33
|
+
'18': '2025-01-01',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Supported Pro templates.
|
|
38
|
+
*/
|
|
39
|
+
const PRO_TEMPLATES = ['fleet', 'hr', 'inventory', 'pos', 'crm'];
|
|
40
|
+
|
|
41
|
+
module.exports = { DEFAULTS, CORE_DEPENDS, COMPATIBILITY_DATES, PRO_TEMPLATES };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pro template metadata.
|
|
5
|
+
* Each entry describes what extra depends, models, and features
|
|
6
|
+
* a Pro template adds on top of the base scaffold.
|
|
7
|
+
*/
|
|
8
|
+
const PRO_TEMPLATE_CONFIG = {
|
|
9
|
+
fleet: {
|
|
10
|
+
label: 'Fleet Management',
|
|
11
|
+
description: 'Vehicle tracking, maintenance schedules, fuel logs, driver assignments',
|
|
12
|
+
depends: ['fleet', 'maintenance'],
|
|
13
|
+
models: ['vehicle', 'maintenance_request', 'fuel_log'],
|
|
14
|
+
hasReports: true,
|
|
15
|
+
hasDashboard: true,
|
|
16
|
+
},
|
|
17
|
+
hr: {
|
|
18
|
+
label: 'Human Resources',
|
|
19
|
+
description: 'Employees, contracts, payroll integration, leave management',
|
|
20
|
+
depends: ['hr', 'hr_payroll', 'hr_holidays'],
|
|
21
|
+
models: ['employee_extension', 'contract_extension'],
|
|
22
|
+
hasReports: true,
|
|
23
|
+
hasDashboard: true,
|
|
24
|
+
},
|
|
25
|
+
inventory: {
|
|
26
|
+
label: 'Inventory',
|
|
27
|
+
description: 'Products, lots/serials, barcode scanner, stock moves',
|
|
28
|
+
depends: ['stock', 'product'],
|
|
29
|
+
models: ['product_extension', 'stock_move_extension'],
|
|
30
|
+
hasReports: true,
|
|
31
|
+
hasDashboard: false,
|
|
32
|
+
},
|
|
33
|
+
pos: {
|
|
34
|
+
label: 'Point of Sale',
|
|
35
|
+
description: 'POS session extension, custom payment methods, receipt customization',
|
|
36
|
+
depends: ['point_of_sale'],
|
|
37
|
+
models: ['pos_order_extension', 'pos_config_extension'],
|
|
38
|
+
hasReports: true,
|
|
39
|
+
hasDashboard: false,
|
|
40
|
+
},
|
|
41
|
+
crm: {
|
|
42
|
+
label: 'CRM Pipeline',
|
|
43
|
+
description: 'Leads, pipeline stages, activities, OWL dashboard with charts',
|
|
44
|
+
depends: ['crm', 'sale_crm'],
|
|
45
|
+
models: ['crm_lead_extension', 'crm_stage_extension'],
|
|
46
|
+
hasReports: false,
|
|
47
|
+
hasDashboard: true,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getProTemplateConfig(templateName) {
|
|
52
|
+
return PRO_TEMPLATE_CONFIG[templateName] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { PRO_TEMPLATE_CONFIG, getProTemplateConfig };
|