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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { writeFile } = require('../utils/file-system');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate Odoo XML views: Form, List, Kanban, Search, Menu.
|
|
9
|
+
*/
|
|
10
|
+
async function generateUiLayer(targetDir, config) {
|
|
11
|
+
const odooDir = path.join(targetDir, 'odoo_module');
|
|
12
|
+
const s = config.moduleNameSnake;
|
|
13
|
+
const P = config.moduleNamePascal;
|
|
14
|
+
const M = config.moduleNameOdoo;
|
|
15
|
+
const L = config.moduleNameLabel;
|
|
16
|
+
|
|
17
|
+
logger.verbose('Generating Odoo XML views...');
|
|
18
|
+
|
|
19
|
+
await Promise.all([
|
|
20
|
+
writeFile(path.join(odooDir, 'views', `${s}_views.xml`), genViews(s, P, M, L, config)),
|
|
21
|
+
writeFile(path.join(odooDir, 'views', `${s}_menus.xml`), genMenus(s, P, M, L)),
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Views generator ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function genViews(s, P, M, L, config) {
|
|
28
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
29
|
+
<odoo>
|
|
30
|
+
|
|
31
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
32
|
+
SEARCH VIEW
|
|
33
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
34
|
+
<record id="view_${s}_search" model="ir.ui.view">
|
|
35
|
+
<field name="name">${M}.search</field>
|
|
36
|
+
<field name="model">${M}</field>
|
|
37
|
+
<field name="arch" type="xml">
|
|
38
|
+
<search string="Search ${L}">
|
|
39
|
+
<field name="name" string="Name" filter_domain="[('name','ilike',self)]"/>
|
|
40
|
+
<field name="reference" string="Reference"/>
|
|
41
|
+
<field name="user_id" string="Responsible"/>
|
|
42
|
+
<separator/>
|
|
43
|
+
<!-- Status filters -->
|
|
44
|
+
<filter name="filter_draft" string="Draft"
|
|
45
|
+
domain="[('state','=','draft')]"/>
|
|
46
|
+
<filter name="filter_confirmed" string="Confirmed"
|
|
47
|
+
domain="[('state','=','confirmed')]"/>
|
|
48
|
+
<filter name="filter_in_progress" string="In Progress"
|
|
49
|
+
domain="[('state','=','in_progress')]"/>
|
|
50
|
+
<filter name="filter_done" string="Done"
|
|
51
|
+
domain="[('state','=','done')]"/>
|
|
52
|
+
<filter name="filter_cancelled" string="Cancelled"
|
|
53
|
+
domain="[('state','=','cancelled')]"/>
|
|
54
|
+
<separator/>
|
|
55
|
+
<filter name="filter_my" string="My Records"
|
|
56
|
+
domain="[('user_id','=',uid)]"/>
|
|
57
|
+
<filter name="filter_today" string="Starting Today"
|
|
58
|
+
domain="[('date_start','=',context_today().strftime('%Y-%m-%d'))]"/>
|
|
59
|
+
<separator/>
|
|
60
|
+
<filter name="filter_archived" string="Archived"
|
|
61
|
+
domain="[('active','=',False)]"/>
|
|
62
|
+
<!-- Group by -->
|
|
63
|
+
<group expand="0" string="Group By">
|
|
64
|
+
<filter name="group_state" string="Status" context="{'group_by': 'state'}"/>
|
|
65
|
+
<filter name="group_user" string="Responsible" context="{'group_by': 'user_id'}"/>
|
|
66
|
+
<filter name="group_priority" string="Priority" context="{'group_by': 'priority'}"/>
|
|
67
|
+
<filter name="group_date_start" string="Start Date" context="{'group_by': 'date_start:month'}"/>
|
|
68
|
+
</group>
|
|
69
|
+
</search>
|
|
70
|
+
</field>
|
|
71
|
+
</record>
|
|
72
|
+
|
|
73
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
74
|
+
FORM VIEW
|
|
75
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
76
|
+
<record id="view_${s}_form" model="ir.ui.view">
|
|
77
|
+
<field name="name">${M}.form</field>
|
|
78
|
+
<field name="model">${M}</field>
|
|
79
|
+
<field name="arch" type="xml">
|
|
80
|
+
<form string="${L}">
|
|
81
|
+
<header>
|
|
82
|
+
<button name="action_confirm" string="Confirm" type="object"
|
|
83
|
+
class="btn-primary"
|
|
84
|
+
invisible="state != 'draft'"/>
|
|
85
|
+
<button name="action_start" string="Start" type="object"
|
|
86
|
+
class="btn-primary"
|
|
87
|
+
invisible="state != 'confirmed'"/>
|
|
88
|
+
<button name="action_done" string="Mark as Done" type="object"
|
|
89
|
+
class="btn-success"
|
|
90
|
+
invisible="state != 'in_progress'"/>
|
|
91
|
+
<button name="action_cancel" string="Cancel" type="object"
|
|
92
|
+
invisible="state in ('done','cancelled')"/>
|
|
93
|
+
<button name="action_reset_draft" string="Reset to Draft" type="object"
|
|
94
|
+
invisible="state != 'cancelled'"/>
|
|
95
|
+
<field name="state" widget="statusbar"
|
|
96
|
+
statusbar_visible="draft,confirmed,in_progress,done"/>
|
|
97
|
+
</header>
|
|
98
|
+
<sheet>
|
|
99
|
+
<!-- Priority stars -->
|
|
100
|
+
<div class="oe_button_box" name="button_box">
|
|
101
|
+
<button class="oe_stat_button" type="object"
|
|
102
|
+
name="action_open_form" icon="fa-info-circle">
|
|
103
|
+
<div class="o_stat_info">
|
|
104
|
+
<span class="o_stat_text">Details</span>
|
|
105
|
+
</div>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger"
|
|
109
|
+
invisible="active"/>
|
|
110
|
+
<field name="active" invisible="1"/>
|
|
111
|
+
<field name="priority" widget="priority" class="me-3"/>
|
|
112
|
+
<div class="oe_title">
|
|
113
|
+
<label for="name" class="oe_edit_only"/>
|
|
114
|
+
<h1>
|
|
115
|
+
<field name="name" placeholder="Enter ${L} name…"
|
|
116
|
+
class="o_field_text"/>
|
|
117
|
+
</h1>
|
|
118
|
+
<h4 class="text-muted">
|
|
119
|
+
<field name="reference" readonly="1"/>
|
|
120
|
+
</h4>
|
|
121
|
+
</div>
|
|
122
|
+
<group>
|
|
123
|
+
<group string="General">
|
|
124
|
+
<field name="user_id"/>
|
|
125
|
+
<field name="company_id"
|
|
126
|
+
groups="base.group_multi_company"/>
|
|
127
|
+
<field name="tag_ids" widget="many2many_tags"
|
|
128
|
+
options="{'color_field': 'color'}"/>
|
|
129
|
+
</group>
|
|
130
|
+
<group string="Schedule">
|
|
131
|
+
<field name="date_start"/>
|
|
132
|
+
<field name="date_end"/>
|
|
133
|
+
<field name="duration_days" readonly="1"
|
|
134
|
+
invisible="not date_start or not date_end"/>
|
|
135
|
+
</group>
|
|
136
|
+
</group>
|
|
137
|
+
<notebook>
|
|
138
|
+
<page string="Description" name="description">
|
|
139
|
+
<field name="description" placeholder="Add a description…"/>
|
|
140
|
+
</page>
|
|
141
|
+
</notebook>
|
|
142
|
+
</sheet>
|
|
143
|
+
<!-- Chatter -->
|
|
144
|
+
<div class="oe_chatter">
|
|
145
|
+
<field name="message_follower_ids"/>
|
|
146
|
+
<field name="activity_ids"/>
|
|
147
|
+
<field name="message_ids"/>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
</field>
|
|
151
|
+
</record>
|
|
152
|
+
|
|
153
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
154
|
+
LIST VIEW
|
|
155
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
156
|
+
<record id="view_${s}_list" model="ir.ui.view">
|
|
157
|
+
<field name="name">${M}.list</field>
|
|
158
|
+
<field name="model">${M}</field>
|
|
159
|
+
<field name="arch" type="xml">
|
|
160
|
+
<list string="${L}"
|
|
161
|
+
decoration-muted="state == 'cancelled'"
|
|
162
|
+
decoration-success="state == 'done'"
|
|
163
|
+
decoration-warning="state == 'in_progress'">
|
|
164
|
+
<field name="priority" widget="priority"/>
|
|
165
|
+
<field name="reference" readonly="1"/>
|
|
166
|
+
<field name="name"/>
|
|
167
|
+
<field name="user_id" optional="show"/>
|
|
168
|
+
<field name="date_start" optional="show"/>
|
|
169
|
+
<field name="date_end" optional="show"/>
|
|
170
|
+
<field name="duration_days" string="Days" optional="hide"/>
|
|
171
|
+
<field name="state" widget="badge"
|
|
172
|
+
decoration-info="state == 'draft'"
|
|
173
|
+
decoration-primary="state == 'confirmed'"
|
|
174
|
+
decoration-warning="state == 'in_progress'"
|
|
175
|
+
decoration-success="state == 'done'"
|
|
176
|
+
decoration-danger="state == 'cancelled'"/>
|
|
177
|
+
<!-- Inline actions -->
|
|
178
|
+
<button name="action_confirm" string="Confirm" type="object"
|
|
179
|
+
icon="fa-check" invisible="state != 'draft'"
|
|
180
|
+
title="Confirm this record"/>
|
|
181
|
+
<button name="action_done" string="Done" type="object"
|
|
182
|
+
icon="fa-flag-checkered" invisible="state != 'in_progress'"
|
|
183
|
+
title="Mark as done"/>
|
|
184
|
+
</list>
|
|
185
|
+
</field>
|
|
186
|
+
</record>
|
|
187
|
+
|
|
188
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
189
|
+
KANBAN VIEW
|
|
190
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
191
|
+
<record id="view_${s}_kanban" model="ir.ui.view">
|
|
192
|
+
<field name="name">${M}.kanban</field>
|
|
193
|
+
<field name="model">${M}</field>
|
|
194
|
+
<field name="arch" type="xml">
|
|
195
|
+
<kanban default_group_by="state"
|
|
196
|
+
class="o_kanban_small_column"
|
|
197
|
+
quick_create="false">
|
|
198
|
+
<field name="id"/>
|
|
199
|
+
<field name="name"/>
|
|
200
|
+
<field name="state"/>
|
|
201
|
+
<field name="priority"/>
|
|
202
|
+
<field name="user_id"/>
|
|
203
|
+
<field name="date_start"/>
|
|
204
|
+
<field name="date_end"/>
|
|
205
|
+
<field name="color"/>
|
|
206
|
+
<templates>
|
|
207
|
+
<t t-name="card" class="oe_kanban_card oe_kanban_global_click">
|
|
208
|
+
<div class="o_kanban_record_top">
|
|
209
|
+
<div class="o_kanban_record_headings">
|
|
210
|
+
<strong class="o_kanban_record_title">
|
|
211
|
+
<field name="name"/>
|
|
212
|
+
</strong>
|
|
213
|
+
</div>
|
|
214
|
+
<field name="priority" widget="priority"/>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="o_kanban_record_body">
|
|
217
|
+
<div t-if="record.date_start.raw_value" class="text-muted small">
|
|
218
|
+
<i class="fa fa-calendar me-1"/>
|
|
219
|
+
<field name="date_start"/>
|
|
220
|
+
<t t-if="record.date_end.raw_value">
|
|
221
|
+
→ <field name="date_end"/>
|
|
222
|
+
</t>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="o_kanban_record_bottom">
|
|
226
|
+
<div class="oe_kanban_bottom_left">
|
|
227
|
+
<field name="state" widget="badge"/>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="oe_kanban_bottom_right">
|
|
230
|
+
<field name="user_id" widget="many2one_avatar_user"/>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</t>
|
|
234
|
+
</templates>
|
|
235
|
+
</kanban>
|
|
236
|
+
</field>
|
|
237
|
+
</record>
|
|
238
|
+
|
|
239
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
240
|
+
ACTIVITY VIEW
|
|
241
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
242
|
+
<record id="view_${s}_activity" model="ir.ui.view">
|
|
243
|
+
<field name="name">${M}.activity</field>
|
|
244
|
+
<field name="model">${M}</field>
|
|
245
|
+
<field name="arch" type="xml">
|
|
246
|
+
<activity string="${L}">
|
|
247
|
+
<field name="user_id"/>
|
|
248
|
+
<templates>
|
|
249
|
+
<div t-name="activity-box">
|
|
250
|
+
<img t-att-src="activity_image('res.users','avatar_128',record.user_id.raw_value)"
|
|
251
|
+
role="img" t-att-title="record.user_id.value"
|
|
252
|
+
t-att-alt="record.user_id.value"/>
|
|
253
|
+
<div>
|
|
254
|
+
<field name="name" display="full"/>
|
|
255
|
+
<field name="state" muted="1" display="full"/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</templates>
|
|
259
|
+
</activity>
|
|
260
|
+
</field>
|
|
261
|
+
</record>
|
|
262
|
+
|
|
263
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
264
|
+
ACTION WINDOW
|
|
265
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
266
|
+
<record id="action_${s}" model="ir.actions.act_window">
|
|
267
|
+
<field name="name">${L}</field>
|
|
268
|
+
<field name="res_model">${M}</field>
|
|
269
|
+
<field name="view_mode">list,kanban,form,activity</field>
|
|
270
|
+
<field name="search_view_id" ref="view_${s}_search"/>
|
|
271
|
+
<field name="context">{
|
|
272
|
+
'search_default_filter_my': 0
|
|
273
|
+
}</field>
|
|
274
|
+
<field name="help" type="html">
|
|
275
|
+
<p class="o_view_nocontent_smiling_face">
|
|
276
|
+
Create your first ${L}!
|
|
277
|
+
</p>
|
|
278
|
+
<p>
|
|
279
|
+
Generated by <a href="https://create-odoo-module.dev" target="_blank">create-odoo-module</a>.
|
|
280
|
+
</p>
|
|
281
|
+
</field>
|
|
282
|
+
</record>
|
|
283
|
+
|
|
284
|
+
</odoo>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function genMenus(s, P, M, L) {
|
|
289
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
290
|
+
<odoo>
|
|
291
|
+
|
|
292
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
293
|
+
TOP-LEVEL MENU
|
|
294
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
295
|
+
<menuitem
|
|
296
|
+
id="menu_${s}_root"
|
|
297
|
+
name="${L}"
|
|
298
|
+
sequence="50"
|
|
299
|
+
web_icon="${s},static/description/icon.png"/>
|
|
300
|
+
|
|
301
|
+
<!-- ══════════════════════════════════════════════════════════════════════
|
|
302
|
+
SUB-MENUS
|
|
303
|
+
══════════════════════════════════════════════════════════════════ -->
|
|
304
|
+
<menuitem
|
|
305
|
+
id="menu_${s}_main"
|
|
306
|
+
name="${L}"
|
|
307
|
+
parent="menu_${s}_root"
|
|
308
|
+
sequence="10"/>
|
|
309
|
+
|
|
310
|
+
<menuitem
|
|
311
|
+
id="menu_${s}_list"
|
|
312
|
+
name="All ${L}s"
|
|
313
|
+
parent="menu_${s}_main"
|
|
314
|
+
action="action_${s}"
|
|
315
|
+
sequence="10"/>
|
|
316
|
+
|
|
317
|
+
<!-- Configuration menu (example) -->
|
|
318
|
+
<menuitem
|
|
319
|
+
id="menu_${s}_config"
|
|
320
|
+
name="Configuration"
|
|
321
|
+
parent="menu_${s}_root"
|
|
322
|
+
sequence="90"
|
|
323
|
+
groups="base.group_system"/>
|
|
324
|
+
|
|
325
|
+
</odoo>
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = { generateUiLayer };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const ejs = require('ejs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Write a file, creating parent directories automatically.
|
|
9
|
+
*/
|
|
10
|
+
async function writeFile(filePath, content) {
|
|
11
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
12
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render an EJS template string and write the output file.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} templatePath Absolute path to .ejs template
|
|
19
|
+
* @param {string} outputPath Destination file path
|
|
20
|
+
* @param {object} data Template variables
|
|
21
|
+
*/
|
|
22
|
+
async function renderTemplate(templatePath, outputPath, data) {
|
|
23
|
+
const template = await fs.readFile(templatePath, 'utf8');
|
|
24
|
+
const rendered = ejs.render(template, data, {
|
|
25
|
+
filename: templatePath, // allows <%- include() %>
|
|
26
|
+
rmWhitespace: false,
|
|
27
|
+
});
|
|
28
|
+
await writeFile(outputPath, rendered);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy a static (non-EJS) file.
|
|
33
|
+
*/
|
|
34
|
+
async function copyFile(src, dest) {
|
|
35
|
+
await fs.ensureDir(path.dirname(dest));
|
|
36
|
+
await fs.copy(src, dest);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Copy an entire directory.
|
|
41
|
+
*/
|
|
42
|
+
async function copyDir(src, dest) {
|
|
43
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create directory (recursive).
|
|
48
|
+
*/
|
|
49
|
+
async function mkdirp(dirPath) {
|
|
50
|
+
await fs.ensureDir(dirPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a path exists.
|
|
55
|
+
*/
|
|
56
|
+
async function exists(p) {
|
|
57
|
+
return fs.pathExists(p);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Remove a path (file or dir).
|
|
62
|
+
*/
|
|
63
|
+
async function remove(p) {
|
|
64
|
+
return fs.remove(p);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { writeFile, renderTemplate, copyFile, copyDir, mkdirp, exists, remove };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const logger = require('./logger');
|
|
6
|
+
|
|
7
|
+
const LICENSE_API = 'https://create-odoo-module.dev/api/v1/license/verify';
|
|
8
|
+
const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 h
|
|
9
|
+
|
|
10
|
+
let _cachedResult = null;
|
|
11
|
+
let _cacheTime = 0;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Verify a Pro license key against the API.
|
|
15
|
+
* Returns true if valid, false otherwise.
|
|
16
|
+
* Results are cached for 24 hours per process.
|
|
17
|
+
*
|
|
18
|
+
* @param {string|null} key
|
|
19
|
+
* @returns {Promise<boolean>}
|
|
20
|
+
*/
|
|
21
|
+
async function verifyProLicense(key) {
|
|
22
|
+
if (!key) return false;
|
|
23
|
+
|
|
24
|
+
// Cache hit
|
|
25
|
+
if (_cachedResult !== null && Date.now() - _cacheTime < CACHE_TTL) {
|
|
26
|
+
return _cachedResult;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
logger.verbose(`Verifying Pro license key: ${key.slice(0, 8)}...`);
|
|
31
|
+
|
|
32
|
+
const response = await axios.post(
|
|
33
|
+
LICENSE_API,
|
|
34
|
+
{ key },
|
|
35
|
+
{ timeout: 5000, headers: { 'User-Agent': 'create-odoo-module-cli' } }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const valid = response.data?.valid === true;
|
|
39
|
+
_cachedResult = valid;
|
|
40
|
+
_cacheTime = Date.now();
|
|
41
|
+
|
|
42
|
+
if (valid) {
|
|
43
|
+
logger.success(chalk.green('Pro license verified ✓'));
|
|
44
|
+
} else {
|
|
45
|
+
logger.warn(`Invalid Pro key. Get your key at ${chalk.underline('https://create-odoo-module.dev/pro')}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return valid;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Network error — fail open (allow generation without Pro features)
|
|
51
|
+
logger.verbose(`License check failed (network): ${err.message}`);
|
|
52
|
+
logger.warn('Could not reach license server — Pro templates skipped');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { verifyProLicense };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const figures = require('figures');
|
|
5
|
+
|
|
6
|
+
let verboseMode = false;
|
|
7
|
+
|
|
8
|
+
const logger = {
|
|
9
|
+
setVerbose(v) { verboseMode = v; },
|
|
10
|
+
isVerbose() { return verboseMode; },
|
|
11
|
+
|
|
12
|
+
info (msg) { console.log(` ${chalk.cyan(figures.info)} ${msg}`); },
|
|
13
|
+
success(msg) { console.log(` ${chalk.green(figures.tick)} ${msg}`); },
|
|
14
|
+
warn (msg) { console.warn(` ${chalk.yellow(figures.warning)} ${chalk.yellow(msg)}`); },
|
|
15
|
+
error (msg) { console.error(` ${chalk.red(figures.cross)} ${chalk.red(msg)}`); },
|
|
16
|
+
dim (msg) { console.log(chalk.dim(` ${msg}`)); },
|
|
17
|
+
blank () { console.log(); },
|
|
18
|
+
|
|
19
|
+
verbose(msg) {
|
|
20
|
+
if (verboseMode) {
|
|
21
|
+
console.log(chalk.dim(` [verbose] ${msg}`));
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
step(index, total, label) {
|
|
26
|
+
console.log(
|
|
27
|
+
` ${chalk.dim(`[${index}/${total}]`)} ${chalk.bold(label)}`
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
module.exports = logger;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a pre-configured spinner instance.
|
|
7
|
+
*/
|
|
8
|
+
function createSpinner(text = '') {
|
|
9
|
+
return ora({
|
|
10
|
+
text,
|
|
11
|
+
spinner: 'dots2',
|
|
12
|
+
color: 'magenta',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an async function with a spinner.
|
|
18
|
+
* Shows `startText` while running, `doneText` on success.
|
|
19
|
+
*/
|
|
20
|
+
async function withSpinner(startText, doneText, fn) {
|
|
21
|
+
const spinner = createSpinner(startText).start();
|
|
22
|
+
try {
|
|
23
|
+
const result = await fn();
|
|
24
|
+
spinner.succeed(doneText || startText);
|
|
25
|
+
return result;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { createSpinner, withSpinner };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fleet-manager → fleet_manager
|
|
5
|
+
* FleetManager → fleet_manager
|
|
6
|
+
*/
|
|
7
|
+
function toSnakeCase(str) {
|
|
8
|
+
return str
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(/[-\s]+/g, '_')
|
|
11
|
+
.replace(/([A-Z])/g, (m, l) => '_' + l.toLowerCase())
|
|
12
|
+
.replace(/^_/, '')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* fleet-manager → FleetManager
|
|
18
|
+
* fleet_manager → FleetManager
|
|
19
|
+
*/
|
|
20
|
+
function toPascalCase(str) {
|
|
21
|
+
return str
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
24
|
+
.replace(/^(.)/, (m, c) => c.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* FleetManager → fleet-manager
|
|
29
|
+
* fleet_manager → fleet-manager
|
|
30
|
+
*/
|
|
31
|
+
function toKebabCase(str) {
|
|
32
|
+
return str
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/([A-Z])/g, (m, l) => '-' + l.toLowerCase())
|
|
35
|
+
.replace(/[_\s]+/g, '-')
|
|
36
|
+
.replace(/^-/, '')
|
|
37
|
+
.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* fleet-manager → Fleet Manager
|
|
42
|
+
*/
|
|
43
|
+
function toTitleCase(str) {
|
|
44
|
+
return toKebabCase(str)
|
|
45
|
+
.split('-')
|
|
46
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
47
|
+
.join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* fleet.manager (Odoo technical name, dot-notation)
|
|
52
|
+
* fleet-manager → fleet.manager
|
|
53
|
+
*/
|
|
54
|
+
function toOdooModel(str) {
|
|
55
|
+
return toSnakeCase(str).replace(/_/g, '.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pad string on the right to a given length
|
|
60
|
+
*/
|
|
61
|
+
function padRight(str, len, char = ' ') {
|
|
62
|
+
return String(str).padEnd(len, char);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { toSnakeCase, toPascalCase, toKebabCase, toTitleCase, toOdooModel, padRight };
|