@webship.co/workspace-odoo 0.0.0-stage → 1.0.8
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/README.md +234 -2
- package/ai/agents/odoo-site-builder.md +139 -0
- package/ai/agents/odoo-tester.md +145 -0
- package/ai/commands/odoo-test.md +88 -0
- package/ai/prompts/odoo-build-and-test.md +27 -0
- package/ai/skills/odoo-scenarios/SKILL.md +234 -0
- package/ai/skills/odoo-scenarios/examples/apps-and-views.feature +36 -0
- package/ai/skills/odoo-scenarios/examples/contact-lifecycle.feature +29 -0
- package/ai/skills/odoo-scenarios/examples/website.feature +43 -0
- package/commands/cmd-odoo-project.sh +34 -0
- package/commands/cmd-tools-odoo-cli.sh +26 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +51 -0
- package/dist/src/index.js.map +1 -0
- package/docs/plugin-items-pattern.md +145 -0
- package/features/odoo.feature +30 -0
- package/package.json +96 -4
- package/scripts/args/arg-odoo-cli.sh +28 -0
- package/scripts/args/arg-odoo.sh +28 -0
- package/scripts/functions/fun-odoo.sh +435 -0
- package/scripts/odoo-accounts.py +129 -0
- package/testing/site.feature +64 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# The accounts of an Odoo site, and what keeps it local. Runs inside `odoo shell` in the Odoo
|
|
2
|
+
# container of the project (the shell provides `env` and `odoo`); the functions of the plugin
|
|
3
|
+
# (fun-odoo.sh) pipe it there and hand it what it needs in two variables:
|
|
4
|
+
#
|
|
5
|
+
# ODOO_ACCOUNTS_VERB one or more of local, owner, add, cancel, list, apps, with commas, run in
|
|
6
|
+
# that order
|
|
7
|
+
# ODOO_ACCOUNTS base64 of a JSON object:
|
|
8
|
+
# { owner: { name, email, password },
|
|
9
|
+
# accounts: [{ name, email, password, role }] }
|
|
10
|
+
#
|
|
11
|
+
# local The site stays local. The "partner_autocomplete" module, which Odoo installs on its
|
|
12
|
+
# own, sends what a user types in a company name to an outside service and mixes the
|
|
13
|
+
# answers into the proposals of the field. A local site must not depend on, or send
|
|
14
|
+
# data to, an outside service, so the module is uninstalled.
|
|
15
|
+
# owner The administrator Odoo created (admin / admin) becomes the workspace account.
|
|
16
|
+
# add The default accounts, each with the groups of its role. One that exists (the demo
|
|
17
|
+
# data has a portal user) gets its groups and its password again and keeps its name.
|
|
18
|
+
# cancel The default accounts are deleted; one that is part of the data of the site (the demo
|
|
19
|
+
# portal user), or that Odoo will not delete because records point at it, is archived.
|
|
20
|
+
# list Who is there. apps The applications that are installed.
|
|
21
|
+
#
|
|
22
|
+
# Every verb can run again. No password is ever printed.
|
|
23
|
+
|
|
24
|
+
import base64
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
from odoo import SUPERUSER_ID, api
|
|
29
|
+
|
|
30
|
+
GIVEN = json.loads(base64.b64decode(os.environ.get('ODOO_ACCOUNTS') or 'e30=').decode('utf-8'))
|
|
31
|
+
VERBS = [verb for verb in os.environ.get('ODOO_ACCOUNTS_VERB', '').split(',') if verb]
|
|
32
|
+
|
|
33
|
+
# The groups of each role, by external id. A group of an app that is not installed is left out.
|
|
34
|
+
ROLES = {
|
|
35
|
+
# Every app that is installed, and the settings.
|
|
36
|
+
'administrator': ['base.group_user', 'base.group_system', 'base.group_partner_manager', 'sales_team.group_sale_manager',
|
|
37
|
+
'account.group_account_manager', 'website.group_website_designer', 'event.group_event_manager',
|
|
38
|
+
'hr_recruitment.group_hr_recruitment_manager'],
|
|
39
|
+
# Sales and CRM, own documents only; no Invoicing.
|
|
40
|
+
'salesperson': ['base.group_user', 'sales_team.group_sale_salesman'],
|
|
41
|
+
# Invoicing; no Sales, no CRM.
|
|
42
|
+
'accountant': ['base.group_user', 'account.group_account_invoice'],
|
|
43
|
+
# An internal user with no app-specific group.
|
|
44
|
+
'employee': ['base.group_user'],
|
|
45
|
+
# A portal user: the website and /my, never the web client.
|
|
46
|
+
'portal': ['base.group_portal'],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for verb in VERBS:
|
|
50
|
+
Users = env['res.users'].with_context(no_reset_password=True, active_test=False)
|
|
51
|
+
# The field that holds the groups of a user was renamed in a later release.
|
|
52
|
+
groups_field = 'groups_id' if 'groups_id' in Users._fields else 'group_ids'
|
|
53
|
+
|
|
54
|
+
if verb == 'local':
|
|
55
|
+
autocomplete = env['ir.module.module'].search([('name', '=', 'partner_autocomplete'), ('state', '=', 'installed')])
|
|
56
|
+
if autocomplete:
|
|
57
|
+
autocomplete.button_immediate_uninstall()
|
|
58
|
+
print(' partner_autocomplete is uninstalled: nothing a user types is sent to an outside service.')
|
|
59
|
+
# The uninstall reloads the registry: take a fresh environment.
|
|
60
|
+
env = api.Environment(odoo.registry(env.cr.dbname).cursor(), SUPERUSER_ID, {})
|
|
61
|
+
|
|
62
|
+
elif verb == 'owner':
|
|
63
|
+
owner = GIVEN.get('owner') or {}
|
|
64
|
+
admin = env.ref('base.user_admin')
|
|
65
|
+
admin.write({'login': owner['name'], 'password': owner['password'], 'email': owner['email']})
|
|
66
|
+
env.cr.commit()
|
|
67
|
+
print(' the administrator of the site is %s.' % admin.login)
|
|
68
|
+
|
|
69
|
+
elif verb == 'add':
|
|
70
|
+
for person in GIVEN.get('accounts') or []:
|
|
71
|
+
wanted, missing = [], []
|
|
72
|
+
for xmlid in ROLES.get(person['role'], []):
|
|
73
|
+
group = env.ref(xmlid, raise_if_not_found=False)
|
|
74
|
+
(wanted if group else missing).append(group.id if group else xmlid)
|
|
75
|
+
if person['role'] not in ROLES:
|
|
76
|
+
print(' %s: "%s" is not a role this knows (%s).' % (person['name'], person['role'], ', '.join(sorted(ROLES))))
|
|
77
|
+
continue
|
|
78
|
+
values = {'password': person['password'], groups_field: [(6, 0, wanted)], 'active': True}
|
|
79
|
+
user = Users.search([('login', '=', person['name'])], limit=1)
|
|
80
|
+
if user:
|
|
81
|
+
user.write(values)
|
|
82
|
+
state = 'was there: its groups and its password are set again'
|
|
83
|
+
else:
|
|
84
|
+
user = Users.create(dict(values, name=person['name'], login=person['name'], email=person['email']))
|
|
85
|
+
state = 'added'
|
|
86
|
+
env.cr.commit()
|
|
87
|
+
print(' %s %s (%s)%s.' % (person['name'], state, person['role'],
|
|
88
|
+
', without %s (not installed)' % ', '.join(missing) if missing else ''))
|
|
89
|
+
|
|
90
|
+
elif verb == 'cancel':
|
|
91
|
+
for person in GIVEN.get('accounts') or []:
|
|
92
|
+
user = Users.search([('login', '=', person['name'])], limit=1)
|
|
93
|
+
if not user:
|
|
94
|
+
print(' %s is not there.' % person['name'])
|
|
95
|
+
continue
|
|
96
|
+
# An account the data of a module defines (the demo data has a portal user) is part of
|
|
97
|
+
# the site: deleting it breaks the next update of that module, so it is archived.
|
|
98
|
+
if env['ir.model.data'].search_count([('model', '=', 'res.users'), ('res_id', '=', user.id)]):
|
|
99
|
+
user.write({'active': False})
|
|
100
|
+
print(' %s is part of the data of the site, so it is archived instead of deleted.' % person['name'])
|
|
101
|
+
env.cr.commit()
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
with env.cr.savepoint():
|
|
105
|
+
user.unlink()
|
|
106
|
+
print(' %s deleted.' % person['name'])
|
|
107
|
+
except Exception:
|
|
108
|
+
# Records of the site point at the account: Odoo keeps it, so it is archived.
|
|
109
|
+
user.write({'active': False})
|
|
110
|
+
print(' %s has records the site keeps, so it is archived instead of deleted.' % person['name'])
|
|
111
|
+
env.cr.commit()
|
|
112
|
+
|
|
113
|
+
elif verb == 'list':
|
|
114
|
+
system = env.ref('base.group_system')
|
|
115
|
+
for user in Users.search([('share', 'in', [True, False])], order='share, login'):
|
|
116
|
+
if user.id == SUPERUSER_ID:
|
|
117
|
+
continue
|
|
118
|
+
kind = 'portal' if user.share else ('administrator' if system in user[groups_field] else 'internal')
|
|
119
|
+
print(' %-24s %-30s %-14s%s' % (user.login, user.email or '', kind, '' if user.active else ' (archived)'))
|
|
120
|
+
|
|
121
|
+
elif verb == 'apps':
|
|
122
|
+
for module in env['ir.module.module'].search([('state', '=', 'installed'), ('application', '=', True)], order='name'):
|
|
123
|
+
print(' %-28s %s' % (module.name, module.shortdesc))
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
print('odoo-accounts.py: ODOO_ACCOUNTS_VERB is local, owner, add, cancel, list or apps, with commas.')
|
|
127
|
+
|
|
128
|
+
env.cr.commit()
|
|
129
|
+
env.cr.close()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Feature: The site answers and its accounts work
|
|
2
|
+
As the owner of the site
|
|
3
|
+
I want the front page, the login page and each default account to work
|
|
4
|
+
So that a build that came out broken is caught before anything else is tested
|
|
5
|
+
|
|
6
|
+
# Placed by the workspace's testing setup as a starting point: it is yours to change.
|
|
7
|
+
# The accounts are the default ones (cmd-tools-add-users.sh); a scenario logs in by a handle of
|
|
8
|
+
# the users registry in cucumber.js, so no password is written here. The apps named below are
|
|
9
|
+
# the ones the builder installs by default.
|
|
10
|
+
|
|
11
|
+
@critical
|
|
12
|
+
Scenario: The front page is served
|
|
13
|
+
Given I am an anonymous user
|
|
14
|
+
And I am on the homepage
|
|
15
|
+
Then the response status code should be 200
|
|
16
|
+
And I should be on a website page
|
|
17
|
+
|
|
18
|
+
@critical
|
|
19
|
+
Scenario: The login page is served with its form
|
|
20
|
+
Given I am on the login page
|
|
21
|
+
Then the response status code should be 200
|
|
22
|
+
And I should see the login form
|
|
23
|
+
|
|
24
|
+
Scenario: The administrator logs in and has every app
|
|
25
|
+
Given I am a logged in user with the "admin" user
|
|
26
|
+
Then I should be logged in as "administrator"
|
|
27
|
+
And I should be in the web client
|
|
28
|
+
And I should see the "Sales" app
|
|
29
|
+
And I should see the "Invoicing" app
|
|
30
|
+
And I should see the "Settings" app
|
|
31
|
+
|
|
32
|
+
Scenario: A salesperson has Sales and CRM, not Invoicing
|
|
33
|
+
Given I am a logged in user with the "salesperson" user
|
|
34
|
+
Then I should be logged in as "salesperson"
|
|
35
|
+
And I should see the "Sales" app
|
|
36
|
+
And I should see the "CRM" app
|
|
37
|
+
But I should not see the "Invoicing" app
|
|
38
|
+
And I should not see the "Settings" app
|
|
39
|
+
|
|
40
|
+
Scenario: An accountant has Invoicing, not Sales
|
|
41
|
+
Given I am a logged in user with the "accountant" user
|
|
42
|
+
Then I should be logged in as "accountant"
|
|
43
|
+
And I should see the "Invoicing" app
|
|
44
|
+
But I should not see the "Sales" app
|
|
45
|
+
And I should not see the "Settings" app
|
|
46
|
+
|
|
47
|
+
Scenario: An employee has no app of a role
|
|
48
|
+
Given I am a logged in user with the "employee" user
|
|
49
|
+
Then I should be logged in as "employee"
|
|
50
|
+
And I should be in the web client
|
|
51
|
+
But I should not see the "Sales" app
|
|
52
|
+
And I should not see the "Invoicing" app
|
|
53
|
+
And I should not see the "Settings" app
|
|
54
|
+
|
|
55
|
+
Scenario: A portal user never reaches the web client
|
|
56
|
+
Given I am a logged in user with the "portal" user
|
|
57
|
+
When I go to "/web"
|
|
58
|
+
Then I should be in the portal
|
|
59
|
+
And I should not be in the web client
|
|
60
|
+
|
|
61
|
+
Scenario: An anonymous visitor has no session
|
|
62
|
+
Given I am not logged in
|
|
63
|
+
And I am on the homepage
|
|
64
|
+
Then I should not be logged in
|