From c240bb6a207ce9ae4374905b02942db88e3e0fed Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Thu, 26 Sep 2019 15:27:26 +0200 Subject: [PATCH] Refactored configuration provisioning --- codegen.sh | 18 +- compose/autosetup.go | 19 - compose/commands/exporter.go | 79 +- compose/compose.go | 8 +- compose/importer/automation.go | 5 + compose/importer/aux.go | 4 +- compose/importer/module.go | 30 +- compose/importer/module_test.go | 41 +- compose/importer/namespace.go | 4 + compose/importer/page.go | 229 +- ...le_full.yaml => module_fields_as_map.yaml} | 5 +- .../testdata/module_fields_as_slice.yaml | 15 + compose/importer/testdata/page_full.yaml | 8 +- compose/provision-config.go | 47 + messaging/autosetup.go | 55 - messaging/importer/aux.go | 61 + messaging/messaging.go | 13 +- messaging/provision-config.go | 47 + monolith/monolith.go | 8 +- pkg/cli/options/provision.go | 4 +- pkg/cli/runner.go | 12 +- pkg/importer/static_fs.go | 57 + .../compose/src/0000_access_control.yaml | 43 + provision/compose/src/1000_namespace.yaml | 10 + provision/compose/src/1100_modules.yaml | 6164 +++++++++++++++++ provision/compose/src/1200_charts.yaml | 103 + provision/compose/src/1300_scripts.yaml | 1847 +++++ provision/compose/src/1400_pages.yaml | 2363 +++++++ provision/compose/static.go | 6 + .../messaging/src/0000_access_control.yaml | 34 + provision/messaging/src/1000_channels.yaml | 5 + provision/messaging/static.go | 6 + provision/system/src/0000_access_control.yaml | 38 + provision/system/static.go | 6 + provision/update.sh | 22 +- system/auth/external/register.go | 2 +- system/commands/importer.go | 32 +- system/commands/users.go | 37 +- system/importer/aux.go | 56 + system/provision-config.go | 102 + ...tosetup.go => provision-oidc-discovery.go} | 74 +- system/provision-settings.go | 14 + system/system.go | 10 +- tests/messaging/provision_test.go | 4 +- tests/system/provision_test.go | 4 +- 45 files changed, 11328 insertions(+), 423 deletions(-) delete mode 100644 compose/autosetup.go rename compose/importer/testdata/{module_full.yaml => module_fields_as_map.yaml} (88%) create mode 100644 compose/importer/testdata/module_fields_as_slice.yaml create mode 100644 compose/provision-config.go delete mode 100644 messaging/autosetup.go create mode 100644 messaging/importer/aux.go create mode 100644 messaging/provision-config.go create mode 100644 pkg/importer/static_fs.go create mode 100644 provision/compose/src/0000_access_control.yaml create mode 100644 provision/compose/src/1000_namespace.yaml create mode 100644 provision/compose/src/1100_modules.yaml create mode 100644 provision/compose/src/1200_charts.yaml create mode 100644 provision/compose/src/1300_scripts.yaml create mode 100644 provision/compose/src/1400_pages.yaml create mode 100644 provision/compose/static.go create mode 100644 provision/messaging/src/0000_access_control.yaml create mode 100644 provision/messaging/src/1000_channels.yaml create mode 100644 provision/messaging/static.go create mode 100644 provision/system/src/0000_access_control.yaml create mode 100644 provision/system/static.go create mode 100644 system/importer/aux.go create mode 100644 system/provision-config.go rename system/{autosetup.go => provision-oidc-discovery.go} (59%) create mode 100644 system/provision-settings.go diff --git a/codegen.sh b/codegen.sh index 5a64ccdb8..773342113 100755 --- a/codegen.sh +++ b/codegen.sh @@ -120,14 +120,10 @@ function database { } -function files { - yellow "> files" - FOLDERS=$(find . -type d -wholename '*/data') - for FOLDER in $FOLDERS; do - FOLDER=$(dirname $FOLDER) - FOLDER=${FOLDER:2} - echo $FOLDER - cd $FOLDER && $GOPATH/bin/statik -p files -m -Z -f -src=data && cd $_PWD +function provision { + yellow "> provision files" + for FOLDER in system compose messaging; do + $GOPATH/bin/statik -p $FOLDER -m -Z -f -src="./provision/$FOLDER/src" -dest "./provision" done green "OK" } @@ -200,8 +196,8 @@ case ${1:-"all"} in database) database ;; - files) - files + provision) + provision ;; specs) specs @@ -212,7 +208,7 @@ case ${1:-"all"} in all) types database - files + provision specs proto esac diff --git a/compose/autosetup.go b/compose/autosetup.go deleted file mode 100644 index 0b0697a0f..000000000 --- a/compose/autosetup.go +++ /dev/null @@ -1,19 +0,0 @@ -package compose - -import ( - "context" - - "github.com/spf13/cobra" - - "github.com/cortezaproject/corteza-server/compose/service" - "github.com/cortezaproject/corteza-server/pkg/cli" -) - -func accessControlSetup(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - c.InitServices(ctx, c) - - // Calling grant directly on internal permissions service to avoid AC check for "grant" - var p = service.DefaultPermissions - var ac = service.DefaultAccessControl - return p.Grant(ctx, ac.Whitelist(), ac.DefaultRules()...) -} diff --git a/compose/commands/exporter.go b/compose/commands/exporter.go index fa82108d2..74afcd8a1 100644 --- a/compose/commands/exporter.go +++ b/compose/commands/exporter.go @@ -9,6 +9,7 @@ import ( "strings" sqlTypes "github.com/jmoiron/sqlx/types" + "github.com/pkg/errors" "github.com/spf13/cobra" "gopkg.in/yaml.v2" @@ -47,6 +48,10 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command { nsOut = Namespace{} ) + if nsFlag == "" { + cli.HandleError(errors.New("Specify namespace to export from")) + } + if namespaceID, _ := strconv.ParseUint(nsFlag, 10, 64); namespaceID > 0 { ns, err = service.DefaultNamespace.FindByID(namespaceID) if err != repository.ErrNamespaceNotFound { @@ -65,8 +70,8 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command { // // Roles are use for resolving access control roles = sysTypes.RoleSet{ - &sysTypes.Role{ID: 1, Handle: "everyone"}, - &sysTypes.Role{ID: 2, Handle: "admins"}, + &sysTypes.Role{ID: permissions.EveryoneRoleID, Handle: "everyone"}, + &sysTypes.Role{ID: permissions.AdminRoleID, Handle: "admins"}, } modules, _, err := service.DefaultModule.Find(types.ModuleFilter{NamespaceID: ns.ID}) @@ -90,13 +95,13 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command { y := yaml.NewEncoder(cmd.OutOrStdout()) - nsOut.Name = ns.Name - nsOut.Handle = ns.Slug - nsOut.Enabled = ns.Enabled - nsOut.Meta = ns.Meta - - nsOut.Allow = expResourcePermissions(permissions.Allow, ns.PermissionResource()) - nsOut.Deny = expResourcePermissions(permissions.Deny, ns.PermissionResource()) + // nsOut.Name = ns.Name + // nsOut.Handle = ns.Slug + // nsOut.Enabled = ns.Enabled + // nsOut.Meta = ns.Meta + // + // nsOut.Allow = expResourcePermissions(permissions.Allow, ns.PermissionResource()) + // nsOut.Deny = expResourcePermissions(permissions.Deny, ns.PermissionResource()) for _, arg := range args { switch arg { @@ -114,14 +119,15 @@ func Exporter(ctx context.Context, c *cli.Config) *cobra.Command { } } - out.Namespaces[ns.Slug] = nsOut + // out.Namespaces[ns.Slug] = nsOut + nsOut.Namespace = ns.Slug _, _ = y, out - cli.HandleError(y.Encode(out)) + cli.HandleError(y.Encode(nsOut)) }, } - cmd.Flags().String("namespace", "crm", "Export namespace resources (by ID or string)") + cmd.Flags().String("namespace", "", "Export namespace resources (by ID or string)") return cmd } @@ -138,13 +144,16 @@ type ( } Namespace struct { + // This is used when exporting one single namespace + Namespace string `yaml:",omitempty"` + Name string `yaml:",omitempty"` Handle string `yaml:",omitempty"` Enabled bool `yaml:",omitempty"` Meta types.NamespaceMeta `yaml:",omitempty"` Modules map[string]Module `yaml:",omitempty"` - Pages map[string]Page `yaml:",omitempty"` + Pages yaml.MapSlice `yaml:",omitempty"` Charts map[string]Chart `yaml:",omitempty"` Scripts map[string]Script `yaml:",omitempty"` @@ -155,7 +164,7 @@ type ( Module struct { Name string Meta string `yaml:"meta,omitempty"` - Fields map[string]Field + Fields yaml.MapSlice Allow map[string][]string `yaml:",omitempty"` Deny map[string][]string `yaml:",omitempty"` @@ -184,7 +193,7 @@ type ( Blocks types.PageBlocks `yaml:",omitempty"` - Pages map[string]Page `yaml:",omitempty"` + Pages yaml.MapSlice `yaml:",omitempty"` Visible bool @@ -275,21 +284,24 @@ func expModuleMetaCleanup(meta sqlTypes.JSONText) string { return meta.String() } -func expModuleFields(ff types.ModuleFieldSet, modules types.ModuleSet) (o map[string]Field) { - o = make(map[string]Field) +func expModuleFields(ff types.ModuleFieldSet, modules types.ModuleSet) (o yaml.MapSlice) { + o = make(yaml.MapSlice, len(ff)) - for _, f := range ff { - o[f.Name] = Field{ - Label: f.Label, - Kind: f.Kind, - Options: expModuleFieldOptions(f, modules), - Private: f.Private, - Required: f.Required, - Visible: f.Visible, - Multi: f.Multi, + for i, f := range ff { + o[i] = yaml.MapItem{ + Key: f.Name, + Value: Field{ + Label: f.Label, + Kind: f.Kind, + Options: expModuleFieldOptions(f, modules), + Private: f.Private, + Required: f.Required, + Visible: f.Visible, + Multi: f.Multi, - Allow: expResourcePermissions(permissions.Allow, types.ModuleFieldPermissionResource), - Deny: expResourcePermissions(permissions.Deny, types.ModuleFieldPermissionResource), + Allow: expResourcePermissions(permissions.Allow, types.ModuleFieldPermissionResource), + Deny: expResourcePermissions(permissions.Deny, types.ModuleFieldPermissionResource), + }, } } @@ -352,12 +364,12 @@ func expModuleFieldOptions(f *types.ModuleField, modules types.ModuleSet) types. return out } -func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet, scripts automation.ScriptSet) (o map[string]Page) { +func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, charts types.ChartSet, scripts automation.ScriptSet) (o yaml.MapSlice) { var ( children = pages.FindByParent(parentID) handle string ) - o = map[string]Page{} + o = yaml.MapSlice{} for _, child := range children { page := Page{ @@ -388,7 +400,10 @@ func expPages(parentID uint64, pages types.PageSet, modules types.ModuleSet, cha pagesHandles[handle] = true - o[handle] = page + o = append(o, yaml.MapItem{ + Key: handle, + Value: page, + }) } return @@ -698,7 +713,7 @@ func makeHandleFromName(name, currentHandle, def string, id uint64) string { return currentHandle } - newHandle := strings.ReplaceAll(name, " ", "_") + newHandle := strings.ReplaceAll(strings.Title(name), " ", "") newHandle = regexp.MustCompile(`[^0-9A-Za-z_\-.]+`).ReplaceAllString(newHandle, "") if handle.IsValid(newHandle) { return newHandle diff --git a/compose/compose.go b/compose/compose.go index cb80da608..4ee57a4b7 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -51,8 +51,8 @@ func Configure() *cli.Config { c.InitServices(ctx, c) - if c.ProvisionOpt.AutoSetup { - cli.HandleError(accessControlSetup(ctx, cmd, c)) + if c.ProvisionOpt.Configuration { + cli.HandleError(provisionConfig(ctx, cmd, c)) } go service.Watchers(ctx) @@ -86,8 +86,8 @@ func Configure() *cli.Config { }, }, - ProvisionAccessControl: cli.Runners{ - accessControlSetup, + ProvisionConfig: cli.Runners{ + provisionConfig, }, } } diff --git a/compose/importer/automation.go b/compose/importer/automation.go index 474401783..6b31372af 100644 --- a/compose/importer/automation.go +++ b/compose/importer/automation.go @@ -224,6 +224,11 @@ func (asImp *AutomationScript) Store(ctx context.Context, k automationScriptKeep func (asImp *AutomationScript) resolveRefs() error { for _, ref := range asImp.modRefs { s := asImp.set.FindByName(ref.as, asImp.namespace.ID) + if s == nil { + // try to find it in no-mans land + s = asImp.set.FindByName(ref.as, 0) + } + if s == nil { return errors.Errorf("invalid reference, unknown automation script (%v)", ref) } diff --git a/compose/importer/aux.go b/compose/importer/aux.go index f059b48c0..b03950cf6 100644 --- a/compose/importer/aux.go +++ b/compose/importer/aux.go @@ -30,8 +30,8 @@ func Import(ctx context.Context, ns *types.Namespace, ff ...io.Reader) (err erro // // Roles are use for resolving access control roles = sysTypes.RoleSet{ - &sysTypes.Role{ID: 1, Handle: "everyone"}, - &sysTypes.Role{ID: 2, Handle: "admins"}, + &sysTypes.Role{ID: permissions.EveryoneRoleID, Handle: "everyone"}, + &sysTypes.Role{ID: permissions.AdminRoleID, Handle: "admins"}, } ) diff --git a/compose/importer/module.go b/compose/importer/module.go index 18406ba85..e924874e5 100644 --- a/compose/importer/module.go +++ b/compose/importer/module.go @@ -147,27 +147,7 @@ func (mImp *Module) Cast(handle string, def interface{}) (err error) { func (mImp *Module) castFields(module *types.Module, def interface{}) (err error) { return deinterfacer.Each(def, func(_ int, fieldName string, val interface{}) (err error) { - if fieldKind, ok := val.(string); ok && fieldName != "" { - // Not much more to do here - field := module.Fields.FindByName(fieldName) - if field == nil { - module.Fields = append(module.Fields, &types.ModuleField{ - Kind: fieldKind, - Name: fieldName, - Label: fieldName, - }) - } else { - field.Kind = fieldKind - } - - return - } - - if !deinterfacer.IsMap(def) { - return errors.New("expecting map of values for module field") - } - - deinterfacer.KVsetString(&fieldName, "name", def) + deinterfacer.KVsetString(&fieldName, "name", val) field := module.Fields.FindByName(fieldName) if field == nil { @@ -182,21 +162,27 @@ func (mImp *Module) castFields(module *types.Module, def interface{}) (err error return deinterfacer.Each(val, func(_ int, key string, val interface{}) (err error) { switch key { + case "name": + // already handled + case "label": field.Label = deinterfacer.ToString(val) + case "kind", "type": field.Kind = deinterfacer.ToString(val) case "options": - // @todo ModuleField.Options return mImp.castFieldOptions(field, val) case "private": field.Private = deinterfacer.ToBool(val) + case "required": field.Required = deinterfacer.ToBool(val) + case "visible": field.Visible = deinterfacer.ToBool(val) + case "multi": field.Multi = deinterfacer.ToBool(val) diff --git a/compose/importer/module_test.go b/compose/importer/module_test.go index df540d924..e3cc0c63c 100644 --- a/compose/importer/module_test.go +++ b/compose/importer/module_test.go @@ -9,32 +9,35 @@ import ( ) func TestModuleImport_CastSet(t *testing.T) { - impFixTester(t, "module_full", func(t *testing.T, module *Module) { + stdFieldsAsTest := func(t *testing.T, module *Module) { + // map definition's sort order is unreliable req := require.New(t) req.Len(module.set, 1) tc := module.set.FindByHandle("mod1") req.Equal(tc.Name, "Module with fields") - req.Equal(tc.Fields, types.ModuleFieldSet{ - { - Name: "f1", - Kind: "Number", - Label: "f1", - }, - { + req.Len(tc.Fields, 2) + req.Equal(tc.Fields.FindByName("f1"), &types.ModuleField{ + Name: "f1", + Kind: "Number", + Label: "f1", + }) - Name: "f2", - Kind: "String", - Label: "F2", - Place: 1, - Required: true, - Options: map[string]interface{}{ - "multiLine": true, - "useRichTextEditor": true, - "multiDelimiter": "\n", - }, + req.Equal(tc.Fields.FindByName("f2"), &types.ModuleField{ + Name: "f2", + Kind: "String", + Label: "F2", + Place: 1, + Required: true, + Options: map[string]interface{}{ + "multiLine": true, + "useRichTextEditor": true, + "multiDelimiter": "\n", }, }) - }) + } + + impFixTester(t, "module_fields_as_slice", stdFieldsAsTest) + impFixTester(t, "module_fields_as_map", stdFieldsAsTest) } diff --git a/compose/importer/namespace.go b/compose/importer/namespace.go index 0aaba502f..841538b7a 100644 --- a/compose/importer/namespace.go +++ b/compose/importer/namespace.go @@ -247,14 +247,18 @@ func (nsImp *Namespace) Store(ctx context.Context, nsk namespaceKeeper, mk modul if err = nsImp.modules[handle].Store(ctx, mk); err != nil { return errors.Wrap(err, "could not import modules") } + + nsImp.scripts[handle].namespace = namespace if err = nsImp.scripts[handle].Store(ctx, sk); err != nil { return errors.Wrap(err, "could not import automation scripts") } + nsImp.charts[handle].namespace = namespace if err = nsImp.charts[handle].Store(ctx, ck); err != nil { return errors.Wrap(err, "could not import charts") } + nsImp.pages[handle].namespace = namespace if err = nsImp.pages[handle].Store(ctx, pk); err != nil { return errors.Wrap(err, "could not import pages") } diff --git a/compose/importer/page.go b/compose/importer/page.go index 2695e6169..fafbf8c28 100644 --- a/compose/importer/page.go +++ b/compose/importer/page.go @@ -3,7 +3,6 @@ package importer import ( "context" "fmt" - "sort" "strconv" "github.com/pkg/errors" @@ -131,9 +130,6 @@ func (pImp *Page) cast(parent, handle string, def interface{}) (err error) { pImp.tree[parent] = append(pImp.tree[parent], handle) - // Make pages are always sorted - sort.Strings(pImp.tree[parent]) - if title, ok := def.(string); ok && title != "" { page.Title = title return nil @@ -280,17 +276,29 @@ func (pImp *Page) storeChildren(ctx context.Context, parent string, k pageKeeper return nil } + var parentPage *types.Page + if parent != "" { + parentPage, err = pImp.Get(parent) + if err != nil { + return + } else if parentPage == nil { + return errors.Errorf("could not load parent %q", parent) + } + } + var page *types.Page - for _, child := range children { + for w, child := range children { if page, err = pImp.Get(child); err != nil { return } - if err = pImp.resolveRefs(page); err != nil { - return + if parentPage != nil { + page.SelfID = parentPage.ID } + page.Weight = w + if page.ID == 0 { page.NamespaceID = pImp.namespace.ID page, err = k.Create(page) @@ -314,127 +322,160 @@ func (pImp *Page) storeChildren(ctx context.Context, parent string, k pageKeeper } } + // We do that at the end - and save all pages with resolved references + // + // Many because internal page referencing from page blocks + var refs uint + for _, child := range children { + if page, err = pImp.Get(child); err != nil { + return + } + + if refs, err = pImp.resolveRefs(page); err != nil { + return + } else if refs > 0 { + // make sure we do not get stale-data error + page.UpdatedAt = nil + if _, err = k.Update(page); err != nil { + return errors.Wrap(err, "could not update resolved refs") + } + } + } + return } // Resolve all refs for this page (page module, inside block) -func (pImp *Page) resolveRefs(page *types.Page) error { - if moduleHandle, ok := pImp.modules[page.Handle]; ok { - if module, err := pImp.getModule(moduleHandle); err != nil { - return err - } else if module == nil { - return errors.Wrapf(err, "could not load module %q for page %q", - moduleHandle, page.Handle) - } else { - page.ModuleID = module.ID - } - } +// +// It counts number of resolved refs so that caller can know +// if there is anything to save +func (pImp *Page) resolveRefs(page *types.Page) (uint, error) { + var refs uint - for i, b := range page.Blocks { - if b.Options == nil { - continue - } - - if h, ok := b.Options["module"]; ok { - if refm, err := pImp.getModule(deinterfacer.ToString(h)); err != nil || refm == nil { - return errors.Errorf("could not load module %q for page %q block #%d (%v)", - h, page.Handle, i+1, err) + return refs, func() error { + if moduleHandle, ok := pImp.modules[page.Handle]; ok { + if module, err := pImp.getModule(moduleHandle); err != nil { + return err + } else if module == nil { + return errors.Wrapf(err, "could not load module %q for page %q", + moduleHandle, page.Handle) } else { - b.Options["moduleID"] = strconv.FormatUint(refm.ID, 10) - delete(b.Options, "module") + page.ModuleID = module.ID + refs++ } } - if h, ok := b.Options["page"]; ok { - if refp, err := pImp.Get(deinterfacer.ToString(h)); err != nil || refp == nil { - return errors.Errorf("could not load page %q for page %q block #%d (%v)", - h, page.Handle, i+1, err) - } else { - b.Options["pageID"] = strconv.FormatUint(refp.ID, 10) - delete(b.Options, "page") + for i, b := range page.Blocks { + if b.Options == nil { + continue } - } - if h, ok := b.Options["chart"]; ok { - if refc, err := pImp.getChart(deinterfacer.ToString(h)); err != nil || refc == nil { - return errors.Errorf("could not load chart %q for page %q block #%d (%v)", - h, page.Handle, i+1, err) - } else { - b.Options["chartID"] = strconv.FormatUint(refc.ID, 10) - delete(b.Options, "chart") + if h, ok := b.Options["module"]; ok { + if refm, err := pImp.getModule(deinterfacer.ToString(h)); err != nil || refm == nil { + return errors.Errorf("could not load module %q for page %q block #%d (%v)", + h, page.Handle, i+1, err) + } else { + b.Options["moduleID"] = strconv.FormatUint(refm.ID, 10) + delete(b.Options, "module") + refs++ + } } - } - if b.Kind == "Automation" { - bb := make([]interface{}, 0) - err := deinterfacer.Each(b.Options["buttons"], func(_ int, _ string, btn interface{}) (err error) { - button := map[string]interface{}{} + if h, ok := b.Options["page"]; ok { + if refp, err := pImp.Get(deinterfacer.ToString(h)); err != nil || refp == nil { + return errors.Errorf("could not load page %q for page %q block #%d (%v)", + h, page.Handle, i+1, err) + } else { + b.Options["pageID"] = strconv.FormatUint(refp.ID, 10) + delete(b.Options, "page") + refs++ + } + } - err = deinterfacer.Each(btn, func(_ int, k string, v interface{}) error { - switch k { - case "script": - if s, err := pImp.getScript(deinterfacer.ToString(v)); err != nil || s == nil { - return errors.Errorf("could not load script %q for page %q block #%d (%v)", - v, page.Handle, i+1, err) - } else { - button["scriptID"] = s.ID + if h, ok := b.Options["chart"]; ok { + if refc, err := pImp.getChart(deinterfacer.ToString(h)); err != nil || refc == nil { + return errors.Errorf("could not load chart %q for page %q block #%d (%v)", + h, page.Handle, i+1, err) + } else { + b.Options["chartID"] = strconv.FormatUint(refc.ID, 10) + delete(b.Options, "chart") + refs++ + } + } + + if b.Kind == "Automation" { + bb := make([]interface{}, 0) + err := deinterfacer.Each(b.Options["buttons"], func(_ int, _ string, btn interface{}) (err error) { + button := map[string]interface{}{} + + err = deinterfacer.Each(btn, func(_ int, k string, v interface{}) error { + switch k { + case "script": + if s, err := pImp.getScript(deinterfacer.ToString(v)); err != nil || s == nil { + return errors.Errorf("could not load script %q for page %q block #%d (%v)", + v, page.Handle, i+1, err) + } else { + button["scriptID"] = s.ID + refs++ + } + default: + button[k] = v } - default: - button[k] = v + + return nil + }) + + if err != nil { + return err } + bb = append(bb, button) return nil }) + b.Options["buttons"] = bb + if err != nil { return err } + } else if b.Kind == "Calendar" { + ff := make([]interface{}, 0) + err := deinterfacer.Each(b.Options["feeds"], func(_ int, _ string, def interface{}) (err error) { + feed := map[string]interface{}{} - bb = append(bb, button) - return nil - }) - - b.Options["buttons"] = bb - - if err != nil { - return err - } - } else if b.Kind == "Calendar" { - ff := make([]interface{}, 0) - err := deinterfacer.Each(b.Options["feeds"], func(_ int, _ string, def interface{}) (err error) { - feed := map[string]interface{}{} - - err = deinterfacer.Each(def, func(_ int, k string, v interface{}) error { - switch k { - case "module": - if m, err := pImp.getModule(deinterfacer.ToString(v)); err != nil || m == nil { - return errors.Errorf("could not load module %q for page %q block #%d (%v)", - v, page.Handle, i+1, err) - } else { - feed["moduleID"] = m.ID + err = deinterfacer.Each(def, func(_ int, k string, v interface{}) error { + switch k { + case "module": + if m, err := pImp.getModule(deinterfacer.ToString(v)); err != nil || m == nil { + return errors.Errorf("could not load module %q for page %q block #%d (%v)", + v, page.Handle, i+1, err) + } else { + feed["moduleID"] = m.ID + refs++ + } + default: + feed[k] = v } - default: - feed[k] = v + + return nil + }) + + if err != nil { + return err } + ff = append(ff, feed) return nil }) + b.Options["feeds"] = ff + if err != nil { return err } - - ff = append(ff, feed) - return nil - }) - - b.Options["feeds"] = ff - - if err != nil { - return err } } - } - return nil + return nil + }() } diff --git a/compose/importer/testdata/module_full.yaml b/compose/importer/testdata/module_fields_as_map.yaml similarity index 88% rename from compose/importer/testdata/module_full.yaml rename to compose/importer/testdata/module_fields_as_map.yaml index 562b85ef6..4e4034867 100644 --- a/compose/importer/testdata/module_full.yaml +++ b/compose/importer/testdata/module_fields_as_map.yaml @@ -2,13 +2,14 @@ modules: mod1: name: Module with fields fields: - f1: Number + f1: + kind: Number f2: kind: String label: F2 required: true options: multiLine: true - useRichTextEditor: true multiDelimiter: |2+ + useRichTextEditor: true diff --git a/compose/importer/testdata/module_fields_as_slice.yaml b/compose/importer/testdata/module_fields_as_slice.yaml new file mode 100644 index 000000000..f1d2b699d --- /dev/null +++ b/compose/importer/testdata/module_fields_as_slice.yaml @@ -0,0 +1,15 @@ +modules: + mod1: + name: Module with fields + fields: + - name: f1 + type: Number + - name: f2 + kind: String + label: F2 + required: true + options: + multiLine: true + multiDelimiter: |2+ + + useRichTextEditor: true diff --git a/compose/importer/testdata/page_full.yaml b/compose/importer/testdata/page_full.yaml index 8bd22269c..f459adfc6 100644 --- a/compose/importer/testdata/page_full.yaml +++ b/compose/importer/testdata/page_full.yaml @@ -1,8 +1,8 @@ pages: - root: + - handle: root title: Root page pages: - sub1: + - handle: sub1 title: Sub page 1 blocks: - title: B1 @@ -12,8 +12,8 @@ pages: - title: B2 xywh: [ 11, 12, 13, 14 ] kind: TheTestingKind - sub2: + - handle: sub2 title: Sub page 2 pages: - sub21: + - handle: sub21 title: Sub-sub page 2.1 diff --git a/compose/provision-config.go b/compose/provision-config.go new file mode 100644 index 000000000..afa8168fb --- /dev/null +++ b/compose/provision-config.go @@ -0,0 +1,47 @@ +package compose + +import ( + "context" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/cortezaproject/corteza-server/compose/importer" + "github.com/cortezaproject/corteza-server/compose/service" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" + impAux "github.com/cortezaproject/corteza-server/pkg/importer" + provision "github.com/cortezaproject/corteza-server/provision/compose" +) + +func provisionConfig(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { + c.Log.Debug("running configuration provision") + c.InitServices(ctx, c) + + // Make sure we have all full access for provisioning + ctx = auth.SetSuperUserContext(ctx) + + if provisioned, err := isProvisioned(ctx); err != nil { + return err + } else if provisioned { + c.Log.Debug("configuration already provisioned") + return nil + } + + readers, err := impAux.ReadStatic(provision.Asset) + if err != nil { + return err + } + + return errors.Wrap( + importer.Import(ctx, nil, readers...), + "could not provision configuration for compose service", + ) +} + +// Provision only where there are no namespaces +func isProvisioned(ctx context.Context) (bool, error) { + _, f, err := service.DefaultNamespace.With(ctx).Find(types.NamespaceFilter{}) + return f.Count > 0, err +} diff --git a/messaging/autosetup.go b/messaging/autosetup.go deleted file mode 100644 index deb8d7f85..000000000 --- a/messaging/autosetup.go +++ /dev/null @@ -1,55 +0,0 @@ -package messaging - -import ( - "context" - - "github.com/spf13/cobra" - "github.com/titpetric/factory" - - "github.com/cortezaproject/corteza-server/messaging/repository" - "github.com/cortezaproject/corteza-server/messaging/service" - "github.com/cortezaproject/corteza-server/messaging/types" - "github.com/cortezaproject/corteza-server/pkg/cli" -) - -func accessControlSetup(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - c.InitServices(ctx, c) - - // Calling grant directly on internal permissions service to avoid AC check for "grant" - var p = service.DefaultPermissions - var ac = service.DefaultAccessControl - return p.Grant(ctx, ac.Whitelist(), ac.DefaultRules()...) -} - -// Add default channels when there are none -func makeDefaultChannels(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - db, err := factory.Database.Get(messaging) - if err != nil { - return err - } - - repo := repository.Channel(ctx, db) - - cc, err := repo.Find(nil) - if err != nil { - return err - } - - if len(cc) == 0 { - cc = types.ChannelSet{ - &types.Channel{Name: "General"}, - &types.Channel{Name: "Random"}, - } - - err = cc.Walk(func(c *types.Channel) error { - _, err := repo.Create(c) - return err - }) - - if err != nil { - return err - } - } - - return nil -} diff --git a/messaging/importer/aux.go b/messaging/importer/aux.go new file mode 100644 index 000000000..d3eba1fc1 --- /dev/null +++ b/messaging/importer/aux.go @@ -0,0 +1,61 @@ +package importer + +import ( + "context" + "io" + + "gopkg.in/yaml.v2" + + "github.com/cortezaproject/corteza-server/internal/permissions" + "github.com/cortezaproject/corteza-server/messaging/service" + "github.com/cortezaproject/corteza-server/messaging/types" + sysTypes "github.com/cortezaproject/corteza-server/system/types" +) + +// Import performs standard import procedure with default services +func Import(ctx context.Context, ff ...io.Reader) (err error) { + var ( + cc types.ChannelSet + aux interface{} + ) + + cc, err = service.DefaultChannel.With(ctx).Find(&types.ChannelFilter{}) + if err != nil { + return err + } + + var ( + p = permissions.NewImporter(service.DefaultAccessControl.Whitelist()) + imp = NewImporter( + p, + NewChannelImport(p, cc), + ) + + // At the moment, we can not load roles from system service + // so we'll just use static set of known roles + // + // Roles are use for resolving access control + roles = sysTypes.RoleSet{ + &sysTypes.Role{ID: permissions.EveryoneRoleID, Handle: "everyone"}, + &sysTypes.Role{ID: permissions.AdminRoleID, Handle: "admins"}, + } + ) + + for _, f := range ff { + if err = yaml.NewDecoder(f).Decode(&aux); err != nil { + return + } + + err = imp.Cast(aux) + if err != nil { + return + } + } + // Store all imported + return imp.Store( + ctx, + service.DefaultChannel.With(ctx), + service.DefaultAccessControl, + roles, + ) +} diff --git a/messaging/messaging.go b/messaging/messaging.go index 149de5f75..403be9c34 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -54,6 +54,10 @@ func Configure() *cli.Config { cli.HandleError(c.ProvisionMigrateDatabase.Run(ctx, cmd, c)) } + if c.ProvisionOpt.Configuration { + cli.HandleError(provisionConfig(ctx, cmd, c)) + } + c.InitServices(ctx, c) var websocketOpt = options.Websocket(messaging) @@ -64,11 +68,6 @@ func Configure() *cli.Config { PingPeriod: websocketOpt.PingPeriod, }) - if c.ProvisionOpt.AutoSetup { - cli.HandleError(accessControlSetup(ctx, cmd, c)) - cli.HandleError(makeDefaultChannels(ctx, cmd, c)) - } - go service.Watchers(ctx) return nil }, @@ -93,8 +92,8 @@ func Configure() *cli.Config { }, }, - ProvisionAccessControl: cli.Runners{ - accessControlSetup, + ProvisionConfig: cli.Runners{ + provisionConfig, }, } } diff --git a/messaging/provision-config.go b/messaging/provision-config.go new file mode 100644 index 000000000..082be4e43 --- /dev/null +++ b/messaging/provision-config.go @@ -0,0 +1,47 @@ +package messaging + +import ( + "context" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/messaging/importer" + "github.com/cortezaproject/corteza-server/messaging/service" + "github.com/cortezaproject/corteza-server/messaging/types" + "github.com/cortezaproject/corteza-server/pkg/cli" + impAux "github.com/cortezaproject/corteza-server/pkg/importer" + provision "github.com/cortezaproject/corteza-server/provision/messaging" +) + +func provisionConfig(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { + c.Log.Debug("running configuration provision") + c.InitServices(ctx, c) + + // Make sure we have all full access for provisioning + ctx = auth.SetSuperUserContext(ctx) + + if provisioned, err := isProvisioned(ctx); err != nil { + return err + } else if provisioned { + c.Log.Debug("configuration already provisioned") + return nil + } + + readers, err := impAux.ReadStatic(provision.Asset) + if err != nil { + return err + } + + return errors.Wrap( + importer.Import(ctx, readers...), + "could not provision configuration for messaging service", + ) +} + +// Provision ONLY when there are no channels (even if we find delete channels we abort provisioning +func isProvisioned(ctx context.Context) (bool, error) { + cc, err := service.DefaultChannel.With(ctx).Find(&types.ChannelFilter{IncludeDeleted: true}) + return len(cc) > 0, err +} diff --git a/monolith/monolith.go b/monolith/monolith.go index 69c58fb14..acdeef1c9 100644 --- a/monolith/monolith.go +++ b/monolith/monolith.go @@ -111,11 +111,11 @@ func Configure() *cli.Config { }, }, - ProvisionAccessControl: cli.Runners{ + ProvisionConfig: cli.Runners{ func(ctx context.Context, cmd *cobra.Command, c *cli.Config) (err error) { - cli.HandleError(cmp.ProvisionAccessControl.Run(ctx, cmd, cmp)) - cli.HandleError(msg.ProvisionAccessControl.Run(ctx, cmd, msg)) - cli.HandleError(sys.ProvisionAccessControl.Run(ctx, cmd, sys)) + cli.HandleError(cmp.ProvisionConfig.Run(ctx, cmd, cmp)) + cli.HandleError(msg.ProvisionConfig.Run(ctx, cmd, msg)) + cli.HandleError(sys.ProvisionConfig.Run(ctx, cmd, sys)) return }, }, diff --git a/pkg/cli/options/provision.go b/pkg/cli/options/provision.go index 06948f433..02fb024e3 100644 --- a/pkg/cli/options/provision.go +++ b/pkg/cli/options/provision.go @@ -3,14 +3,14 @@ package options type ( ProvisionOpt struct { MigrateDatabase bool `env:"PROVISION_MIGRATE_DATABASE"` - AutoSetup bool `env:"PROVISION_AUTO_SETUP"` + Configuration bool `env:"PROVISION_CONFIGURATION"` } ) func Provision(pfix string) (o *ProvisionOpt) { o = &ProvisionOpt{ MigrateDatabase: true, - AutoSetup: true, + Configuration: true, } fill(o, pfix) diff --git a/pkg/cli/runner.go b/pkg/cli/runner.go index 634cb7197..07d846400 100644 --- a/pkg/cli/runner.go +++ b/pkg/cli/runner.go @@ -95,7 +95,7 @@ type ( // Access control initial setup // Reapplies default access control rules for roles "everyone" [1] and "admin" [2] - ProvisionAccessControl Runners + ProvisionConfig Runners // ****************************************************************** @@ -264,7 +264,7 @@ func (c *Config) MakeCLI(ctx context.Context) (cmd *cobra.Command) { cmd.AddCommand(serveApiCmd) - if len(c.ProvisionMigrateDatabase) > 0 || len(c.ProvisionAccessControl) > 0 { + if len(c.ProvisionMigrateDatabase) > 0 || len(c.ProvisionConfig) > 0 { var ( provisionCmd = &cobra.Command{ Use: "provision", @@ -275,17 +275,17 @@ func (c *Config) MakeCLI(ctx context.Context) (cmd *cobra.Command) { // Add only commands with defined callbacks if len(c.ProvisionMigrateDatabase) > 0 { provisionCmd.AddCommand(&cobra.Command{ - Use: "access-control-rules", - Short: "Reset access control rules & roles", + Use: "configuration", + Short: "Create permissions & resources", RunE: func(cmd *cobra.Command, args []string) error { - return c.ProvisionAccessControl.Run(ctx, nil, c) + return c.ProvisionConfig.Run(ctx, nil, c) }, }) } // Add only commands with defined callbacks - if len(c.ProvisionAccessControl) > 0 { + if len(c.ProvisionConfig) > 0 { provisionCmd.AddCommand(&cobra.Command{ Use: "migrate-database", Short: "Run database migration scripts", diff --git a/pkg/importer/static_fs.go b/pkg/importer/static_fs.go new file mode 100644 index 000000000..0a4b16b59 --- /dev/null +++ b/pkg/importer/static_fs.go @@ -0,0 +1,57 @@ +package importer + +import ( + "bytes" + "io" + "os" + "path/filepath" + "sort" + + "github.com/goware/statik/fs" + "github.com/pkg/errors" +) + +// ReadStatic reads static FS and returns slice of io.Readers +func ReadStatic(data string) ([]io.Reader, error) { + var ( + files = make([]string, 0) + + yamlFilter = func(filename string, info os.FileInfo, err error) error { + if err == nil && !info.IsDir() { + if matched, err := filepath.Match("/*.yaml", filename); matched && err == nil { + files = append(files, filename) + } + } + + return err + } + + sfs, err = fs.New(data) + + readers []io.Reader + ) + + if err != nil { + return nil, errors.Wrap(err, "could not read static filesystem") + } + + if err = fs.Walk(sfs, "/", yamlFilter); err != nil { + return nil, errors.Wrap(err, "could not filter files") + } + + if len(files) == 0 { + return nil, nil + } + + sort.Strings(files) + + for _, file := range files { + if bb, err := fs.ReadFile(sfs, file); err != nil { + return nil, errors.Wrapf(err, "could not read %s", file) + } else { + readers = append(readers, bytes.NewBuffer(bb)) + } + } + + return readers, nil +} diff --git a/provision/compose/src/0000_access_control.yaml b/provision/compose/src/0000_access_control.yaml new file mode 100644 index 000000000..c64ce55b1 --- /dev/null +++ b/provision/compose/src/0000_access_control.yaml @@ -0,0 +1,43 @@ +allow: + everyone: + compose: + - access + + admins: + compose: + - access + - grant + - namespace.create + + compose:namespace: + - read + - update + - delete + - manage + - page.create + - module.create + - chart.create + + compose:module: + - read + - update + - delete + - record.create + - record.read + - record.update + - record.delete + + compose:chart: + - read + - update + - delete + + compose:page: + - read + - update + - delete + + compose:automation-script: + - read + - update + - delete diff --git a/provision/compose/src/1000_namespace.yaml b/provision/compose/src/1000_namespace.yaml new file mode 100644 index 000000000..7551edcdc --- /dev/null +++ b/provision/compose/src/1000_namespace.yaml @@ -0,0 +1,10 @@ +namespaces: + crm: + name: CRM + allow: + everyone: + - module.create + - read + deny: + everyone: + - delete diff --git a/provision/compose/src/1100_modules.yaml b/provision/compose/src/1100_modules.yaml new file mode 100644 index 000000000..328ff25b2 --- /dev/null +++ b/provision/compose/src/1100_modules.yaml @@ -0,0 +1,6164 @@ +namespace: crm +modules: + Account: + name: Account + fields: + AccountName: + label: Account Name + kind: String + required: true + AccountSource: + label: Account Source + kind: Select + options: + options: + - Customer Referral + - Direct + - Employee Referral + - Event + - Existing Customer + - Inbound Call + - Outbound List + - Partner Referral + - Seminar + - Social Media + - Webinar + - Website (Organic) + - Website (PPC) + - Word of Mouth + - Other + selectType: default + AnnualRevenue: + label: Annual Revenue + kind: Select + options: + options: + - Less Than 500,000 + - 500,000 To 1 Million + - 1 To 2.5 Million + - 2.5 To 5 Million + - 5 To 10 Million + - 10 To 20 Million + - 20 To 50 Million + - 50 To 100 Million + - 100 To 500 Million + selectType: default + BillingCity: + label: Billing City + kind: String + BillingCountry: + label: Billing Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + selectType: default + BillingPostalCode: + label: Billing Postal Code + kind: String + BillingState: + label: Billing State + kind: String + BillingStreet: + label: Billing Street + kind: String + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + Facebook: + label: Facebook + kind: String + Fax: + label: Fax + kind: String + Industry: + label: Industry + kind: Select + options: + options: + - "" + - Agriculture + - Apparel + - Banking + - Biotechnology + - Chemicals + - Communications + - Construction + - Consulting + - Education + - Electronics + - Energy + - Engineering + - Entertainment + - Environmental + - Finance + - Food & Beverage + - Government + - Healthcare + - Hospitality + - Insurance + - Machinery + - Manufacturing + - Media + - Not For Profit + - Other + - Recreation + - Retail + - Shipping + - Technology + - Telecommunications + - Transportation + - Utilities + selectType: default + LastViewedDate: + label: Last Viewed Date + kind: DateTime + LinkedIn: + label: LinkedIn + kind: String + NumberOfEmployees: + label: Employees + kind: Number + options: + format: 0 + precision: 0 + OwnerId: + label: Account Owner + kind: User + options: + presetWithAuthenticated: false + selectType: default + Ownership: + label: Ownership + kind: Select + options: + options: + - Public + - Private + selectType: default + ParentId: + label: Parent Account + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + selectType: default + Phone: + label: Phone + kind: String + Rating: + label: Rating + kind: Select + options: + options: + - ☆☆☆☆☆ + - ★☆☆☆☆ + - ★★☆☆☆ + - ★★★☆☆ + - ★★★★☆ + - ★★★★★ + selectType: default + ShippingCity: + label: Shipping City + kind: String + ShippingCountry: + label: Shipping Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + selectType: default + ShippingPostalCode: + label: Shipping Postal Code + kind: String + ShippingState: + label: Shipping State + kind: String + ShippingStreet: + label: Shipping Street + kind: String + Sic: + label: SIC Code + kind: String + SicDesc: + label: SIC Description + kind: String + Twitter: + label: Twitter + kind: String + Type: + label: Type + kind: Select + options: + options: + - Channel Partner / Reseller + - Customer - Channel + - Customer - Direct + - Installation Partner + - Pending + - Prospect + - Technology Partner + - Other + selectType: default + Website: + label: Website + kind: String + AccountContactRole: + name: AccountContactRole + fields: + AccountId: + label: Account + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + ContactId: + label: Contact + kind: Record + options: + labelField: MasterRecordId + module: Contact + queryFields: + - AccountId + - MasterRecordId + IsPrimary: + label: Primary + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Role: + label: Role + kind: Select + options: + options: + - Business User + - Decision Maker + - Economic Buyer + - Economic Decision Maker + - Evaluator + - Executive Sponsor + - Gatekeeper + - Influencer + - Project Manager + - Technical Buyer + - Other + CampaignMember: + name: CampaignMember + fields: + CampaignId: + label: Campaign + kind: Record + options: + labelField: Name + module: Campaigns + queryFields: + - Name + ContactId: + label: Contact + kind: Record + options: + labelField: Email + module: Contact + queryFields: + - AccountId + - Email + - FirstName + - LastName + FirstRespondedDate: + label: First Responded Date + kind: DateTime + HasResponded: + label: Responded + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LeadId: + label: Lead + kind: Record + options: + labelField: Email + module: Lead + queryFields: + - Email + - FirstName + - LastName + Status: + label: Status + kind: Select + options: + options: + - Attended + - Registered + - Responded + - Viewed a Demo + - Visited Booth + - Viewed a Webinar + Campaigns: + name: Campaigns + fields: + ActualCost: + label: Actual Cost + kind: Number + options: + format: 0 + precision: 0 + AmountAllOpportunities: + label: Value Opportunities in Campaign + kind: Number + options: + format: 0 + precision: 0 + AmountWonOpportunities: + label: Value Won Opportunities in Campaign + kind: Number + options: + format: 0 + precision: 0 + BudgetedCost: + label: Budgeted Cost + kind: Number + options: + format: 0 + precision: 0 + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + EndDate: + label: End Date + kind: DateTime + options: + onlyDate: true + ExpectedResponse: + label: Expected Response (%) + kind: Number + options: + format: 0 + precision: 0 + ExpectedRevenue: + label: Expected Revenue + kind: Number + options: + format: 0 + precision: 0 + IsActive: + label: Active + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LastActivityDate: + label: Last Activity + kind: DateTime + LastModifiedById: + label: Last Modified By + kind: User + options: + presetWithAuthenticated: false + LastModifiedDate: + label: Last Modified Date + kind: DateTime + LastViewedDate: + label: Last Viewed Date + kind: DateTime + Name: + label: Campaign Name + kind: String + NumberOfContacts: + label: Contacts in Campaign + kind: Number + options: + format: 0 + precision: 0 + NumberOfConvertedLeads: + label: Converted Leads in Campaign + kind: Number + options: + format: 0 + precision: 0 + NumberOfLeads: + label: Leads in Campaign + kind: Number + options: + format: 0 + precision: 0 + NumberOfOpportunities: + label: Opportunities in Campaign + kind: Number + options: + format: 0 + precision: 0 + NumberOfResponses: + label: Responses in Campaign + kind: Number + options: + format: 0 + precision: 0 + NumberOfWonOpportunities: + label: Won Opportunities in Campaign + kind: Number + options: + format: 0 + precision: 0 + NumberSent: + label: Num Sent + kind: Number + options: + format: 0 + precision: 0 + OwnerId: + label: Campaign Owner + kind: User + options: + presetWithAuthenticated: false + ParentId: + label: Parent Campaign + kind: Record + options: + module: Campaigns + queryFields: + - Name + StartDate: + label: Start Date + kind: DateTime + options: + onlyDate: true + Status: + label: Status + kind: Select + options: + options: + - Aborted + - Completed + - In Progress + - Planned + - Tentative + Type: + label: Type + kind: Select + options: + options: + - Lead Generation - Direct Mail + - Lead Generation - Conference + - Lead Generation - Email Blast + - Lead Generation - PPC + - Lead Generation - Seminar + - Lead Generation - Trade Show + - Lead Generation - Webinar + - Lead Generation - Website + - Brand Building - Print advertisement + - Brand Building - Radio advertisement + - Brand Building - TV advertisement + - Brand Building - Sponsoring + - Brand Building - Social Media + - Lead Generation - Contacting personal network + Case: + name: Case + fields: + AccountId: + label: Account Name + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + CaseNumber: + label: Case Number + kind: Number + options: + format: "0000000" + precision: 0 + ClosedDate: + label: Date/Time Closed + kind: DateTime + ContactId: + label: Contact Name + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + File: + label: File + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + IsClosed: + label: Closed + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsClosedOnCreate: + label: Closed When Created + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsEscalated: + label: Escalated + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Origin: + label: Case Origin + kind: Select + options: + options: + - Email + - Messaging + - Phone + - Web + - Other + OwnerId: + label: Case Owner + kind: User + options: + presetWithAuthenticated: false + ParentId: + label: Parent Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + Priority: + label: Priority + kind: Select + options: + options: + - Low + - Medium + - High + ProductId: + label: Product + kind: Record + options: + labelField: Name + module: Product + queryFields: + - ProductCode + - Name + Reason: + label: Case Reason + kind: Select + options: + options: + - Documentation unclear + - Software bug + - User didn't attend training + - Other + SolutionFile: + label: Solution File + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + SolutionId: + label: Solution Record + kind: Record + options: + labelField: SolutionName + module: Solution + queryFields: + - SolutionNote + - SolutionNumber + - SolutionName + SolutionName: + label: Solution Title + kind: String + SolutionNote: + label: Solution Details + kind: String + options: + multiLine: true + useRichTextEditor: true + Status: + label: Status + kind: Select + options: + options: + - New + - Working + - Escalated + - Closed + Subject: + label: Subject + kind: String + SubmitAsSolution: + label: Submit as Solution Record when Case is Closed + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + SuppliedCompany: + label: Supplied Company + kind: String + SuppliedEmail: + label: Supplied Email + kind: String + SuppliedName: + label: Supplied Name + kind: String + SuppliedPhone: + label: Supplied Phone + kind: String + Type: + label: Type + kind: Select + options: + options: + - Bug + - Feature Request + - Question + - Other + CaseContactRole: + name: CaseContactRole + fields: + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - CaseNumber + - Subject + ContactId: + label: Contact + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + IsPrimary: + label: Primary + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Role: + label: Role + kind: Select + options: + options: + - Business User + - Decision Maker + - Economic Buyer + - Economic Decision Maker + - Evaluator + - Executive Sponsor + - Gatekeeper + - Influencer + - Project Manager + - Technical Buyer + - Other + CaseMilestone: + name: CaseMilestone + fields: + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + CompletionDate: + label: Completion Date + kind: DateTime + IsCompleted: + label: Completed + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsViolated: + label: Violation + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + MilestoneTypeId: + label: Milestone Type + kind: Record + options: + labelField: Name + module: MilestoneType + queryFields: + - Name + StartDate: + label: Start Date + kind: DateTime + TargetDate: + label: Target Date + kind: DateTime + TargetResponseInDays: + label: Target Response (Days) + kind: Number + options: + format: 0 + precision: 0 + TargetResponseInHrs: + label: Target Response (Hours) + kind: Number + options: + format: 0 + precision: 0 + TargetResponseInMins: + label: Target Response (Mins) + kind: Number + options: + format: 0 + precision: 0 + CaseUpdate: + name: CaseUpdate + fields: + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + File: + label: File + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + IsSolution: + label: Marked as solution + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Type: + label: Type + kind: Select + options: + options: + - Email + - Call + - Fax + - Letter + - Visit + Contact: + name: Contact + fields: + AccountId: + label: Account Name + kind: Record + options: + labelField: AccountName + module: Account + queryFields: [] + AssistantName: + label: Assistant + kind: String + AssistantPhone: + label: Asst. Phone + kind: String + Birthdate: + label: Birthdate + kind: DateTime + private: true + Department: + label: Department + kind: String + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + DoNotCall: + label: Do Not Call + kind: Bool + options: + falseLabel: "False" + trueLabel: Do Not Call + Email: + label: Email + kind: Email + private: true + Facebook: + label: Facebook + kind: Url + private: true + Fax: + label: Fax + kind: String + private: true + FirstName: + label: First Name + kind: String + private: true + HasOptedOutOfEmail: + label: Email Opt Out + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + HasOptedOutOfFax: + label: Fax Opt Out + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + HomePhone: + label: Home Phone + kind: String + private: true + IsDeleted: + label: Deleted + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsPrimary: + label: Primary contact for account + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LastName: + label: Last Name + kind: String + private: true + LastViewedDate: + label: Last Viewed Date + kind: DateTime + LeadSource: + label: Lead Source + kind: Select + options: + options: + - Customer Referral + - Direct + - Employee Referral + - Event + - Existing Customer + - Inbound Call + - Outbound List + - Partner Referral + - Seminar + - Social Media + - Webinar + - Website (Organic) + - Website (PPC) + - Word of Mouth + - Other + LinkedIn: + label: LinkedIn + kind: Url + private: true + MailingCity: + label: Mailing City + kind: String + private: true + MailingCountry: + label: Mailing Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + private: true + MailingPostalCode: + label: Mailing Postal Code + kind: String + private: true + MailingState: + label: Mailing State + kind: String + private: true + MailingStreet: + label: Mailing Street + kind: String + private: true + MasterRecordId: + label: Contact + kind: Record + options: + labelField: Email + module: Contact + queryFields: [] + MobilePhone: + label: Mobile + kind: String + private: true + OtherCity: + label: Other City + kind: String + private: true + OtherCountry: + label: Other Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + private: true + OtherPhone: + label: Other Phone + kind: String + private: true + OtherPostalCode: + label: Other Postal Code + kind: String + private: true + OtherState: + label: Other State + kind: String + private: true + OtherStreet: + label: Other Street + kind: String + private: true + OwnerId: + label: Contact Owner + kind: User + options: + presetWithAuthenticated: false + Phone: + label: Phone + kind: String + private: true + PhotoUrl: + label: Photo URL + kind: Url + private: true + RecordLabel: + label: Record Label + kind: String + private: true + ReportsToId: + label: Reports To + kind: Record + options: + labelField: Email + module: Contact + queryFields: + - AccountId + - Email + - FirstName + - LastName + Salutation: + label: Salutation + kind: String + private: true + Title: + label: Title + kind: String + private: true + Twitter: + label: Twitter + kind: Url + private: true + Contract: + name: Contract + fields: + AccountId: + label: Account Name + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + ActivatedById: + label: Activated By + kind: Record + options: + labelField: Email + module: Contact + queryFields: + - AccountId + - Email + - FirstName + - LastName + ActivatedDate: + label: Activated Date + kind: DateTime + BillingCity: + label: Billing City + kind: String + private: true + BillingCountry: + label: Billing Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + private: true + BillingPostalCode: + label: Billing Postal Code + kind: String + private: true + BillingState: + label: Billing State + kind: String + private: true + BillingStreet: + label: Billing Street + kind: String + private: true + CompanySignedDate: + label: Company Signed Date + kind: DateTime + CompanySignedId: + label: Company Signed By + kind: Record + options: + labelField: Email + module: Contact + queryFields: + - AccountId + - Email + - FirstName + - LastName + ContractNumber: + label: Contract Number + kind: Number + options: + format: "0000000" + precision: 0 + ContractTerm: + label: Term (months) + kind: Number + options: + format: 0 + precision: 0 + CustomerSignedDate: + label: Customer Signed Date + kind: DateTime + CustomerSignedId: + label: Customer Signed By + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + CustomerSignedTitle: + label: Customer Signed Title + kind: String + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + EndDate: + label: End Date + kind: DateTime + options: + onlyDate: true + File1: + label: File 1 + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + File2: + label: File 2 + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + File3: + label: File 3 + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + File4: + label: File 4 + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + LastActivityDate: + label: Last Activity + kind: DateTime + LastApprovedDate: + label: Last Approved Date + kind: DateTime + Name: + label: Contract Name + kind: String + OwnerExpirationNotice: + label: Owner Expiration Notice + kind: Select + options: + options: + - 3 Days + - 5 Days + - 7 Days + - 14 Days + - 30 Days + - 45 Days + - 60 Days + - 75 Days + - 90 Days + - 120 Days + - 180 Days + - 240 Days + - 300 Days + - 365 Days + OwnerId: + label: Contract Owner + kind: User + options: + presetWithAuthenticated: false + RecordLabel: + label: Record Label + kind: String + ShippingCity: + label: Shipping City + kind: String + private: true + ShippingCountry: + label: Shipping Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + private: true + ShippingPostalCode: + label: Shipping Postal Code + kind: String + private: true + ShippingState: + label: Shipping State + kind: String + private: true + ShippingStreet: + label: Shipping Street + kind: String + private: true + SpecialTerms: + label: Special Terms + kind: String + options: + multiLine: true + useRichTextEditor: true + StartDate: + label: Start Date + kind: DateTime + options: + onlyDate: true + Status: + label: Status + kind: Select + options: + options: + - Draft + - Activated + - In Approval Process + - Inactive (Expired) + - Inactive (Deactivated) + ContractContactRole: + name: ContractContactRole + fields: + ContactId: + label: Contact + kind: Record + options: + labelField: Email + module: Contact + queryFields: + - AccountId + - Email + - FirstName + - LastName + ContractId: + label: Contract + kind: Record + options: + labelField: Name + module: Contract + queryFields: + - AccountId + - Name + - ContractNumber + IsPrimary: + label: Primary + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Role: + label: Role + kind: Select + options: + options: + - Business User + - Decision Maker + - Economic Buyer + - Economic Decision Maker + - Evaluator + - Executive Sponsor + - Gatekeeper + - Influencer + - Project Manager + - Technical Buyer + - Other + ContractLineItem: + name: ContractLineItem + fields: + ContractId: + label: 'Contract ' + kind: Record + options: + labelField: Name + module: Contract + queryFields: + - AccountId + - Name + - ContractNumber + Description: + label: Description + kind: String + options: + multiLine: true + Discount: + label: Discount + kind: Number + options: + format: 0 + precision: 0 + EndDate: + label: End Date + kind: DateTime + options: + onlyDate: true + LineItemNumber: + label: Line Item Number + kind: String + ListPrice: + label: List Price + kind: Number + options: + format: 0 + precision: "2" + suffix: € + PricebookEntryId: + label: Price Book Entry + kind: Record + options: + labelField: Name + module: PricebookEntry + queryFields: + - ProductCode + - Name + Quantity: + label: Quantity + kind: Number + options: + format: 0 + precision: 0 + StartDate: + label: Start Date + kind: DateTime + options: + onlyDate: true + Status: + label: Status + kind: Select + options: + options: + - Active + - Expired + - Inactive + Subtotal: + label: 'Subtotal ' + kind: Number + options: + format: 0 + precision: "2" + suffix: € + TotalPrice: + label: Total Price + kind: Number + options: + format: 0 + precision: "2" + suffix: € + UnitPrice: + label: Sales Price + kind: Number + options: + format: 0 + precision: 2 + suffix: € + DefaultSettings: + name: DefaultSettings + fields: + CaseNextNumber: + label: Next number for a case (updates after saving a new case) + kind: Number + options: + format: "0000000" + precision: 0 + ContractDefaultTime: + label: Default time for a contract (in months) + kind: Number + options: + format: 0 + precision: 0 + ContractNextNumber: + label: Next number for a contract (updates after saving a new contract) + kind: Number + options: + format: "0000000" + precision: 0 + OpportunityCloseDateDays: + label: Days for an opportunity to close + kind: Number + options: + format: 0 + precision: 0 + OpportunityForecaseCategory: + label: Forecast Category + kind: String + OpportunityProbability: + label: Probability + kind: Number + options: + format: 0 + precision: 0 + suffix: '%' + OpportunityStagename: + label: Stage + kind: String + QuoteExpirationDays: + label: Days for a quote to expire + kind: Number + options: + format: 0 + precision: 0 + QuoteNextNumber: + label: Next number for a quote (updates after saving a new quote) + kind: Number + options: + format: "0000000" + precision: 0 + SolutionNextNumber: + label: Next number for a solution (updates after saving a new solution) + kind: Number + options: + format: "0000000" + precision: 0 + EmailTemplate: + name: EmailTemplate + fields: + Body: + label: Body + kind: String + options: + multiLine: true + useRichTextEditor: true + Description: + label: Description + kind: String + options: + multiLine: true + DeveloperName: + label: Template Unique Name + kind: String + options: + multiLine: true + useRichTextEditor: true + IsActive: + label: Available For Use + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LastUsedDate: + label: Last Used Date + kind: DateTime + Markup: + label: Markup + kind: String + options: + multiLine: true + useRichTextEditor: true + Name: + label: Email Template Name + kind: String + Subject: + label: Subject + kind: String + TemplateStyle: + label: Style + kind: Select + options: + options: + - "" + TemplateType: + label: Template Type + kind: Select + options: + options: + - "" + Entitlement: + name: Entitlement + fields: + AccountId: + label: Account Name + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + CasesPerEntitlement: + label: Cases Per Entitlement + kind: Number + options: + format: 0 + precision: 0 + ContractLineItemId: + label: Contract Line Item + kind: Record + options: + labelField: ContractId + module: ContractLineItem + queryFields: + - Description + - LineItemNumber + - ContractId + EndDate: + label: End Date + kind: DateTime + EntitlementProcess: + label: Entitlement Process + kind: String + IsPerIncident: + label: Per Incident + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LastModifiedById: + label: Last Modified By + kind: User + options: + presetWithAuthenticated: false + LastModifiedDate: + label: Last Modified Date + kind: DateTime + LastViewedDate: + label: Last Viewed Date + kind: DateTime + Name: + label: Entitlement Name + kind: String + options: + multiLine: true + useRichTextEditor: true + RemainingCases: + label: Remaining Cases + kind: Number + options: + format: 0 + precision: 0 + StartDate: + label: Start Date + kind: DateTime + Status: + label: Status + kind: Select + options: + options: + - Active + - Expired + - Inactive + Type: + label: Type + kind: Select + options: + options: + - Web Support + - Phone Support + - Email Support + - Dedicated Support + EntitlementContact: + name: EntitlementContact + fields: + CaseMilestoneTypeId: + label: Casemilestone Type + kind: Record + options: + labelField: Name + module: MilestoneType + queryFields: + - Name + ContactId: + label: Contact Name + kind: Record + options: + labelField: Email + module: Contact + queryFields: + - AccountId + - Email + - FirstName + - LastName + EntitlementId: + label: Entitlement Name + kind: Record + options: + labelField: Name + module: Entitlement + queryFields: + - AccountId + - Name + Name: + label: Entitlement Contact + kind: String + options: + multiLine: true + useRichTextEditor: true + EntitlementTemplate: + name: EntitlementTemplate + fields: + BusinessHours: + label: Business Hours + kind: String + CasesPerEntitlement: + label: Cases Per Entitlement + kind: Number + options: + format: 0 + precision: 0 + IsPerIncident: + label: Per Incident + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Name: + label: Entitlement Template Name + kind: String + Term: + label: Term (Days) + kind: Number + options: + format: 0 + precision: 0 + Type: + label: Type + kind: Select + options: + options: + - Email + - Messaging + - Phone + - Web + Event: + name: Event + fields: + AccountId: + label: Account + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + ActivityDate: + label: Start + kind: DateTime + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + ContactId: + label: Contact + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + ContractId: + label: Contract + kind: Record + options: + labelField: Name + module: Contract + queryFields: + - AccountId + - Name + - ContractNumber + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + DurationInMinutes: + label: Duration + kind: Number + options: + format: 0 + precision: 0 + EndDateTime: + label: End + kind: DateTime + IsAllDayEvent: + label: All-Day Event + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsArchived: + label: Archived + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsPrivate: + label: Private + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsReminderSet: + label: Reminder Set + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LeadId: + label: Lead + kind: Record + options: + labelField: RecordLabel + module: Lead + queryFields: + - RecordLabel + Location: + label: Location + kind: String + OpportunityId: + label: Opportunity + kind: Record + options: + labelField: RecordLabel + module: Opportunity + queryFields: + - AccountId + - Name + OwnerId: + label: Assigned To + kind: User + options: + presetWithAuthenticated: false + QuoteId: + label: Quote + kind: Record + options: + labelField: Name + module: Quote + queryFields: + - AccountId + - Name + - QuoteNumber + ReminderDateTime: + label: Reminder Date/Time + kind: DateTime + Subject: + label: Subject + kind: String + Type: + label: Type + kind: Select + options: + options: + - Call + - Email + - Meeting + - Send Letter / Quote + - Other + Idea: + name: Idea + fields: + Body: + label: Idea Body + kind: String + options: + multiLine: true + useRichTextEditor: true + Categories: + label: Categories + kind: Select + options: + options: + - Ecosystem + - Human Resources + - Marketing + - Product + - Sales + - Services + - Support + CreatorName: + label: Name of Creator + kind: String + ParentIdeaId: + label: Parent Idea + kind: Record + options: + labelField: Title + module: Idea + queryFields: + - Title + Status: + label: Status + kind: Select + options: + options: + - New + - Under Consideration + - Accepted + - Implemented + - Duplicate + Title: + label: Title + kind: String + VoteScore: + label: Vote Score + kind: Number + options: + format: 0 + precision: 0 + VoteTotal: + label: Vote Total + kind: Number + options: + format: 0 + precision: 0 + Lead: + name: Lead + fields: + AnnualRevenue: + label: Annual Revenue + kind: Select + options: + options: + - Less Than 500,000 + - 500,000 To 1 Million + - 1 To 2.5 Million + - 2.5 To 5 Million + - 5 To 10 Million + - 10 To 20 Million + - 20 To 50 Million + - 50 To 100 Million + - 100 To 500 Million + selectType: default + City: + label: Address City + kind: String + private: true + Company: + label: Company + kind: String + required: true + ConvertedAccountId: + label: Converted Account + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + selectType: default + ConvertedContactId: + label: Converted Contact + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + selectType: default + ConvertedDate: + label: Converted Date + kind: DateTime + ConvertedOpportunityId: + label: Converted Opportunity + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: + - AccountId + - Name + selectType: default + Country: + label: Address Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + selectType: default + private: true + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + DoNotCall: + label: Do Not Call + kind: Bool + options: + falseLabel: "False" + trueLabel: Do Not Call + Email: + label: Email + kind: String + private: true + Facebook: + label: Facebook + kind: Url + private: true + Fax: + label: Fax + kind: String + private: true + FirstName: + label: First Name + kind: String + private: true + HasOptedOutOfEmail: + label: Email Opt Out + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + HasOptedOutOfFax: + label: Fax Opt Out + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Industry: + label: Industry + kind: Select + options: + options: + - "" + - Agriculture + - Apparel + - Banking + - Biotechnology + - Chemicals + - Communications + - Construction + - Consulting + - Education + - Electronics + - Energy + - Engineering + - Entertainment + - Environmental + - Finance + - Food & Beverage + - Government + - Healthcare + - Hospitality + - Insurance + - Machinery + - Manufacturing + - Media + - Not For Profit + - Other + - Recreation + - Retail + - Shipping + - Technology + - Telecommunications + - Transportation + - Utilities + selectType: default + IsConverted: + label: Converted + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsDeleted: + label: Deleted + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsUnreadByOwner: + label: Unread By Owner + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LastName: + label: Last Name + kind: String + private: true + required: true + LastTransferDate: + label: Last Transfer Date + kind: DateTime + LastViewedDate: + label: Last Viewed Date + kind: DateTime + LeadSource: + label: Lead Source + kind: Select + options: + options: + - Customer Referral + - Direct + - Employee Referral + - Event + - Existing Customer + - Inbound Call + - Outbound List + - Partner Referral + - Seminar + - Social Media + - Webinar + - Website (Organic) + - Website (PPC) + - Word of Mouth + - Other + selectType: default + LinkedIn: + label: LinkedIn + kind: Url + private: true + MasterRecordId: + label: Lead + kind: Record + options: + labelField: Email + module: Lead + queryFields: [] + selectType: default + MobilePhone: + label: Mobile + kind: String + NumberOfEmployees: + label: No. of Employees (exact) + kind: Number + options: + format: 0 + precision: 0 + NumberOfEmployeesRange: + label: No. of Employees (range) + kind: Select + options: + options: + - 1 - 9 + - 10 - 24 + - 25 - 49 + - 50 - 99 + - 100 - 249 + - 250 - 499 + - 500 - 999 + - 1000 - 2499 + - 2500 - 4999 + - 5000 - 9999 + - Over 10000 + selectType: default + OwnerId: + label: Lead Owner + kind: User + options: + presetWithAuthenticated: false + selectType: default + Phone: + label: Phone + kind: String + private: true + PostalCode: + label: Address Postal Code + kind: String + private: true + Rating: + label: Rating + kind: Select + options: + options: + - ☆☆☆☆☆ + - ★☆☆☆☆ + - ★★☆☆☆ + - ★★★☆☆ + - ★★★★☆ + - ★★★★★ + selectType: default + RecordLabel: + label: Record Label + kind: String + private: true + SICCode: + label: SIC Code + kind: Number + options: + format: 0 + precision: 0 + Salutation: + label: Salutation + kind: String + State: + label: Address State + kind: String + private: true + Status: + label: Lead Status + kind: Select + options: + options: + - New + - Assigned + - In Progress + - Converted + - Recycled + - Dead + selectType: default + Street: + label: Address Street + kind: String + private: true + Title: + label: Title + kind: String + Twitter: + label: Twitter + kind: Url + private: true + Website: + label: Website + kind: Url + MilestoneType: + name: MilestoneType + fields: + Description: + label: Description + kind: String + options: + multiLine: true + Name: + label: Name + kind: String + RecurrenceType: + label: Recurrence Type + kind: Select + options: + options: + - No Recurrence + - Independent + - Sequential + Note: + name: Note + fields: + AccountContactRoleId: + label: Account Contact Role + kind: Record + options: + labelField: Role + module: AccountContactRole + queryFields: + - AccountId + - ContactId + - Role + AccountId: + label: Account + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + Body: + label: Body + kind: String + options: + multiLine: true + useRichTextEditor: true + CampaignId: + label: Campaign + kind: Record + options: + labelField: Name + module: Campaigns + queryFields: + - Name + CampaignMemberId: + label: Campaign Member + kind: Record + options: + labelField: CampaignId + module: CampaignMember + queryFields: + - CampaignId + - ContactId + - LeadId + CaseContactRoleId: + label: Case Contact Role + kind: Record + options: + labelField: Role + module: CaseContactRole + queryFields: + - CaseId + - Role + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + CaseMilestoneId: + label: Case Milestone + kind: Record + options: + labelField: CaseId + module: CaseMilestone + queryFields: + - CaseId + - MilestoneTypeId + ContactId: + label: Contact + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + ContractContactRoleId: + label: Contract Contact Role + kind: Record + options: + labelField: ContactId + module: ContractContactRole + queryFields: + - ContactId + - ContractId + ContractId: + label: Contract + kind: Record + options: + labelField: Name + module: Contract + queryFields: + - AccountId + - Name + - ContractNumber + ContractLineItemId: + label: Contract Line Item + kind: Record + options: + labelField: PricebookEntryId + module: ContractLineItem + queryFields: + - PricebookEntryId + EmailMessageId: + label: Email Message + kind: Record + options: + labelField: Subject + module: 'Error: module with ID 69055788596920337 does not exist' + queryFields: + - Subject + EmailTemplateId: + label: Email Template + kind: Record + options: + labelField: Subject + module: EmailTemplate + queryFields: + - Subject + EntitlementContactId: + label: Entitlement Contact + kind: Record + options: + labelField: EntitlementId + module: EntitlementContact + queryFields: + - EntitlementId + EntitlementId: + label: Entitlement + kind: Record + options: + labelField: Name + module: Entitlement + queryFields: + - Name + EntitlementTemplateId: + label: Entitlement Template + kind: Record + options: + labelField: Name + module: EntitlementTemplate + queryFields: + - Name + EventId: + label: Event + kind: Record + options: + labelField: Subject + module: Event + queryFields: + - Subject + FileAdd1: + label: File 1 + kind: File + options: + allowDocuments: false + allowImages: false + mode: grid + FileAdd2: + label: File 2 + kind: File + options: + allowDocuments: false + allowImages: false + mode: grid + FileAdd3: + label: File 3 + kind: File + options: + allowDocuments: false + allowImages: false + mode: grid + FileAdd4: + label: File 4 + kind: File + options: + allowDocuments: false + allowImages: false + mode: grid + IsPrivate: + label: Private + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LeadId: + label: Lead + kind: Record + options: + labelField: RecordLabel + module: Lead + queryFields: + - RecordLabel + OpportunityCompetitorId: + label: Opportunity Competitor + kind: Record + options: + labelField: CompetitorName + module: OpportunityCompetitor + queryFields: + - CompetitorName + OpportunityContactRoleId: + label: Opportunity Contact Role + kind: Record + options: + labelField: ContactId + module: OpportunityContactRole + queryFields: + - ContactId + - Role + OpportunityId: + label: Opportunity + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: + - AccountId + - Name + OpportunityLineItemId: + label: Opportunity Line Item + kind: Record + options: + labelField: ProductId + module: OpportunityLineItem + queryFields: + - Description + - Name + - ProductId + PricebookEntryId: + label: Pricebook Entry + kind: Record + options: + labelField: Name + module: PricebookEntry + queryFields: + - ProductCode + - Name + PricebookId: + label: Pricebook + kind: Record + options: + labelField: Name + module: Pricebook + queryFields: + - Name + ProductId: + label: Product + kind: Record + options: + labelField: Name + module: Product + queryFields: + - ProductCode + - Name + QuoteId: + label: Quote + kind: Record + options: + labelField: Name + module: Quote + queryFields: + - AccountId + - Name + - QuoteNumber + TaskId: + label: Task + kind: Record + options: + labelField: Subject + module: Task + queryFields: + - Subject + Title: + label: Title + kind: String + Opportunity: + name: Opportunity + fields: + AccountId: + label: Account Name + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + selectType: default + Amount: + label: Amount + kind: Number + options: + format: 0 + precision: 2 + suffix: € + Authority: + label: Authority + kind: String + options: + multiLine: true + Budget: + label: Budget + kind: String + options: + multiLine: true + CampaignId: + label: Primary Campaign Source + kind: Record + options: + labelField: Name + module: Campaigns + queryFields: + - Name + selectType: default + CloseDate: + label: Close Date + kind: DateTime + Competition: + label: Competition + kind: String + options: + multiLine: true + ContractId: + label: Contract + kind: Record + options: + labelField: Name + module: Contract + queryFields: + - AccountId + - Name + - ContractNumber + selectType: default + Description: + label: Description + kind: String + options: + multiLine: true + useRichTextEditor: true + ExpectedRevenue: + label: Expected Revenue + kind: Number + options: + format: 0 + precision: 2 + suffix: € + FiscalQuarter: + label: Fiscal Period + kind: Number + options: + format: 0 + precision: 0 + FiscalYear: + label: Fiscal Period + kind: Number + options: + format: 0 + precision: 0 + ForecastCategory: + label: Forecast Category + kind: Select + options: + options: + - Pipeline + - Best Case + - Commit + selectType: default + HasOpportunityLineItem: + label: Has Line Item + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsClosed: + label: Closed + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsPrivate: + label: Private + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsWon: + label: Won + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LeadSource: + label: Lead Source + kind: Select + options: + options: + - Customer Referral + - Direct + - Employee Referral + - Event + - Existing Customer + - Inbound Call + - Outbound List + - Partner Referral + - Seminar + - Social Media + - Webinar + - Website (Organic) + - Website (PPC) + - Word of Mouth + - Other + selectType: default + Name: + label: Opportunity Name + kind: String + Need: + label: Need + kind: String + options: + multiLine: true + NextStep: + label: Next Step + kind: String + OwnerId: + label: Opportunity Owner + kind: User + options: + presetWithAuthenticated: false + selectType: default + PricebookId: + label: Price Book + kind: Record + options: + labelField: Name + module: Pricebook + queryFields: + - Name + selectType: default + Probability: + label: Probability (%) + kind: Number + options: + format: 0 + precision: 0 + suffix: '%' + RecordLabel: + label: Record Label + kind: String + StageName: + label: Stage + kind: Select + options: + options: + - Qualification + - Need Analysis + - Proposal + - Negotiation + - Closed Won + - Closed Lost + selectType: default + Timeline: + label: Timeline + kind: String + options: + multiLine: true + TotalOpportunityQuantity: + label: Quantity + kind: Number + options: + format: 0 + precision: 0 + Type: + label: Type + kind: Select + options: + options: + - Exisiting Business + - New Business + selectType: default + OpportunityCompetitor: + name: OpportunityCompetitor + fields: + CompetitorName: + label: Competitor Name + kind: String + OpportunityId: + label: Opportunity + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: + - AccountId + - Name + Strengths: + label: Strengths + kind: String + options: + multiLine: true + useRichTextEditor: true + Weaknesses: + label: Weaknesses + kind: String + options: + multiLine: true + useRichTextEditor: true + OpportunityContactRole: + name: OpportunityContactRole + fields: + ContactId: + label: Contact + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + IsPrimary: + label: Primary + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + OpportunityId: + label: Opportunity + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: [] + Role: + label: Role + kind: Select + options: + options: + - Business User + - Decision Maker + - Economic Buyer + - Economic Decision Maker + - Evaluator + - Executive Sponsor + - Gatekeeper + - Influencer + - Project Manager + - Technical Buyer + - Other + OpportunityLineItem: + name: OpportunityLineItem + fields: + Description: + label: Line Description + kind: String + Discount: + label: Discount + kind: Number + options: + format: 0 + precision: 2 + suffix: € + ListPrice: + label: List Price + kind: Number + options: + format: 0 + precision: 2 + suffix: € + Name: + label: Opportunity Product Name + kind: String + OpportunityId: + label: Opportunity + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: + - AccountId + - Name + PricebookEntryId: + label: Price Book Entry + kind: Record + options: + labelField: Name + module: PricebookEntry + queryFields: + - ProductCode + - Name + ProductCode: + label: Product Code + kind: String + ProductId: + label: Product + kind: Record + options: + labelField: Name + module: Product + queryFields: [] + Quantity: + label: Quantity + kind: Number + options: + format: 0 + precision: 0 + ServiceDate: + label: Date + kind: DateTime + SortOrder: + label: Sort Order + kind: Number + options: + format: 0 + precision: 0 + Subtotal: + label: Subtotal + kind: Number + options: + format: 0 + precision: 2 + suffix: € + TotalPrice: + label: Total Price + kind: Number + options: + format: 0 + precision: 2 + suffix: € + UnitPrice: + label: Sales Price + kind: Number + options: + format: 0 + precision: 2 + suffix: € + Pricebook: + name: Pricebook + fields: + Description: + label: Description + kind: String + options: + multiLine: true + IsActive: + label: Active + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsStandard: + label: Is Standard Price Book + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Name: + label: Price Book Name + kind: String + PricebookEntry: + name: PricebookEntry + fields: + IsActive: + label: Active + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Name: + label: Product Name + kind: String + PricebookId: + label: Price Book + kind: Record + options: + labelField: Name + module: Pricebook + queryFields: + - Name + ProductCode: + label: Product Code + kind: String + ProductId: + label: Product + kind: Record + options: + labelField: Name + module: Product + queryFields: + - ProductCode + - Name + UnitPrice: + label: List Price + kind: Number + options: + format: 0 + precision: 2 + suffix: € + UseStandardPrice: + label: Use Standard Price + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Product: + name: Product + fields: + Description: + label: Product Description + kind: String + options: + multiLine: true + useRichTextEditor: true + Family: + label: Product Family + kind: Select + options: + options: + - Crust Core + - Crust Add-On + IsActive: + label: Active + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + Name: + label: Product Name + kind: String + ProductCode: + label: Product Code + kind: String + Question: + name: Question + fields: + BestReplyId: + label: Best Reply + kind: Record + options: + labelField: Name + module: Reply + queryFields: + - Name + Body: + label: Question Body + kind: String + options: + multiLine: true + useRichTextEditor: true + NumReplies: + label: Number of Replies + kind: Number + options: + format: 0 + precision: 0 + Title: + label: Question Title + kind: String + UpVotes: + label: Up Votes + kind: Number + options: + format: 0 + precision: 0 + VoteScore: + label: Vote Score + kind: Number + options: + format: 0 + precision: 0 + Quote: + name: Quote + fields: + AccountId: + label: Account Name + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + AdditionalCity: + label: Additional To City + kind: String + AdditionalCountry: + label: Additional To Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + AdditionalName: + label: Additional To Name + kind: String + AdditionalPostalCode: + label: Additional To Postal Code + kind: String + AdditionalState: + label: Additional To State + kind: String + AdditionalStreet: + label: Additional To Street + kind: String + BillingCity: + label: Bill To City + kind: String + BillingCountry: + label: Bill To Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + BillingName: + label: Bill To Name + kind: String + BillingPostalCode: + label: Bill To Postal Code + kind: String + BillingState: + label: Bill To State + kind: String + BillingStreet: + label: Bill To Street + kind: String + ContactId: + label: Contact Name + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + Description: + label: Description + kind: String + options: + multiLine: true + Discount: + label: Additional Discount + kind: Number + options: + format: 0 + precision: 2 + suffix: € + Email: + label: Email + kind: Email + ExpirationDate: + label: Expiration Date + kind: DateTime + Fax: + label: Fax + kind: String + GrandTotal: + label: Grand Total (calculated) + kind: Number + options: + format: 0 + precision: 2 + suffix: € + LineItemCount: + label: Line Items + kind: Number + options: + format: 0 + precision: 0 + Name: + label: Quote Name + kind: String + OpportunityId: + label: Opportunity Name + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: + - AccountId + - Name + Phone: + label: Phone + kind: String + PricebookId: + label: Pricebook + kind: Record + options: + labelField: Name + module: Pricebook + queryFields: + - Name + QuoteNumber: + label: Quote Number + kind: Number + options: + format: "0000000" + precision: 0 + QuoteToCity: + label: Quote To City + kind: String + QuoteToCountry: + label: Quote To Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + QuoteToName: + label: Quote To Name + kind: String + QuoteToPostalCode: + label: Quote To Postal Code + kind: String + QuoteToState: + label: Quote To State + kind: String + QuoteToStreet: + label: Quote To Street + kind: String + RecordLabel: + label: Record Label + kind: String + ShippingCity: + label: Ship To City + kind: String + ShippingCountry: + label: Ship To Country + kind: Select + options: + options: + - Afghanistan + - Åland Islands + - Albania + - Algeria + - American Samoa + - Andorra + - Angola + - Anguilla + - Antarctica + - Antigua And Barbuda + - Argentina + - Armenia + - Aruba + - Australia + - Austria + - Azerbaijan + - Bahamas + - Bahrain + - Bangladesh + - Barbados + - Belarus + - Belgium + - Belize + - Benin + - Bermuda + - Bhutan + - Bolivia + - Bosnia And Herzegovina + - Botswana + - Bouvet Island + - Brazil + - British Indian Ocean Territory + - Brunei Darussalam + - Bulgaria + - Burkina Faso + - Burundi + - Cambodia + - Cameroon + - Canada + - Cape Verde + - Cayman Islands + - Central African Republic + - Chad + - Chile + - China + - Christmas Island + - Cocos (Keeling) Islands + - Colombia + - Comoros + - Congo + - Congo; The Democratic Republic Of The + - Cook Islands + - Costa Rica + - Cote D'ivoire + - Croatia + - Cuba + - Cyprus + - Czechia + - Denmark + - Djibouti + - Dominica + - Dominican Republic + - Ecuador + - Egypt + - El Salvador + - Equatorial Guinea + - Eritrea + - Estonia + - Ethiopia + - Falkland Islands (Malvinas) + - Faroe Islands + - Fiji + - Finland + - France + - French Guiana + - French Polynesia + - French Southern Territories + - Gabon + - Gambia + - Georgia + - Germany + - Ghana + - Gibraltar + - Greece + - Greenland + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-bissau + - Guyana + - Haiti + - Heard Island And Mcdonald Islands + - Holy See (Vatican City State) + - Honduras + - Hong Kong + - Hungary + - Iceland + - India + - Indonesia + - Iran; Islamic Republic Of + - Iraq + - Ireland + - Isle Of Man + - Israel + - Italy + - Jamaica + - Japan + - Jersey + - Jordan + - Kazakhstan + - Kenya + - Kiribati + - Korea; Democratic People's Republic Of + - Korea; Republic Of + - Kuwait + - Kyrgyzstan + - Lao People's Democratic Republic + - Latvia + - Lebanon + - Lesotho + - Liberia + - Libyan Arab Jamahiriya + - Liechtenstein + - Lithuania + - Luxembourg + - Macao + - Macedonia; The Former Yugoslav Republic Of + - Madagascar + - Malawi + - Malaysia + - Maldives + - Mali + - Malta + - Marshall Islands + - Martinique + - Mauritania + - Mauritius + - Mayotte + - Mexico + - Micronesia; Federated States Of + - Moldova; Republic Of + - Monaco + - Mongolia + - Montenegro + - Montserrat + - Morocco + - Mozambique + - Myanmar + - Namibia + - Nauru + - Nepal + - Netherlands + - Netherlands Antilles + - New Caledonia + - New Zealand + - Nicaragua + - Niger + - Nigeria + - Niue + - Norfolk Island + - Northern Mariana Islands + - Norway + - Oman + - Pakistan + - Palau + - Palestinian Territory; Occupied + - Panama + - Papua New Guinea + - Paraguay + - Peru + - Philippines + - Pitcairn + - Poland + - Portugal + - Puerto Rico + - Qatar + - Reunion + - Romania + - Russian Federation + - Rwanda + - Saint Helena + - Saint Kitts And Nevis + - Saint Lucia + - Saint Pierre And Miquelon + - Saint Vincent And The Grenadines + - Samoa + - San Marino + - Sao Tome And Principe + - Saudi Arabia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Slovakia + - Slovenia + - Solomon Islands + - Somalia + - South Africa + - South Georgia And The South Sandwich Islands + - Spain + - Sri Lanka + - Sudan + - Suriname + - Svalbard And Jan Mayen + - Swaziland + - Sweden + - Switzerland + - Syrian Arab Republic + - Taiwan; Province Of China + - Tajikistan + - Tanzania; United Republic Of + - Thailand + - Timor-leste + - Togo + - Tokelau + - Tonga + - Trinidad And Tobago + - Tunisia + - Turkey + - Turkmenistan + - Turks And Caicos Islands + - Tuvalu + - Uganda + - Ukraine + - United Arab Emirates + - United Kingdom + - United States + - United States Minor Outlying Islands + - Uruguay + - Uzbekistan + - Vanuatu + - Venezuela + - Viet Nam + - Virgin Islands; British + - Virgin Islands; U.S. + - Wallis And Futuna + - Western Sahara + - Yemen + - Zambia + - Zimbabwe + ShippingHandling: + label: Shipping and Handling + kind: Number + options: + format: 0 + precision: 2 + suffix: € + ShippingName: + label: Ship To Name + kind: String + ShippingPostalCode: + label: Ship To Postal Code + kind: String + ShippingState: + label: Ship To State + kind: String + ShippingStreet: + label: Ship To Street + kind: String + Status: + label: Status + kind: Select + options: + options: + - Draft + - Needs Review + - In Review + - Approved + - Rejected + - Presented + - Accepted + - Denied + Subtotal: + label: Subtotal (calculated) + kind: Number + options: + format: 0 + precision: 2 + suffix: € + Tax: + label: Tax + kind: Number + options: + format: 0 + precision: 0 + suffix: '%' + TotalPrice: + label: Total Price (calculated) + kind: Number + options: + format: 0 + precision: 2 + suffix: € + QuoteLineItem: + name: QuoteLineItem + fields: + Description: + label: Line Description + kind: String + Discount: + label: Discount + kind: Number + options: + format: 0 + precision: "2" + suffix: € + LineItemDescription: + label: Line Item Description + kind: String + options: + multiLine: true + LineItemNumber: + label: Line Item Number + kind: String + ListPrice: + label: List Price + kind: Number + options: + format: 0 + precision: "2" + suffix: € + PricebookEntryId: + label: Price Book Entry + kind: Record + options: + labelField: Name + module: PricebookEntry + queryFields: + - ProductCode + - Name + ProductCode: + label: Product Code + kind: String + ProductId: + label: Product + kind: Record + options: + labelField: Name + module: Product + queryFields: + - ProductCode + - Name + Quantity: + label: Quantity + kind: Number + options: + format: 0 + precision: 0 + QuoteId: + label: Quote + kind: Record + options: + labelField: Name + module: Quote + queryFields: + - AccountId + - Name + - QuoteNumber + ServiceDate: + label: Date + kind: DateTime + SortOrder: + label: Sort Order + kind: Number + options: + format: 0 + precision: 0 + Subtotal: + label: Subtotal + kind: Number + options: + format: 0 + precision: "2" + suffix: € + TotalPrice: + label: Total Price + kind: Number + options: + format: 0 + precision: "2" + suffix: € + UnitPrice: + label: Sales Price + kind: Number + options: + format: 0 + precision: 2 + suffix: € + Reply: + name: Reply + fields: + Body: + label: Reply Body + kind: String + options: + multiLine: true + useRichTextEditor: true + DownVotes: + label: Down Votes + kind: Number + options: + format: 0 + precision: 0 + IdeaId: + label: Idea + kind: Record + options: + labelField: Title + module: Idea + queryFields: + - Title + Name: + label: Name + kind: String + QuestionId: + label: Question + kind: Record + options: + labelField: Title + module: Question + queryFields: + - Title + UpVotes: + label: Up Votes + kind: Number + options: + format: 0 + precision: 0 + VoteTotal: + label: Vote Total + kind: Number + options: + format: 0 + precision: 0 + Solution: + name: Solution + fields: + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + File: + label: File + kind: File + options: + allowDocuments: false + allowImages: false + mode: list + IsPublished: + label: Public + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsReviewed: + label: Reviewed + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + ProductId: + label: Product + kind: Record + options: + labelField: Name + module: Product + queryFields: + - ProductCode + - Name + SolutionName: + label: Solution Title + kind: String + SolutionNote: + label: Solution Details + kind: String + options: + multiLine: true + useRichTextEditor: true + SolutionNumber: + label: Solution Number + kind: Number + options: + format: "0000000" + precision: 0 + Status: + label: Status + kind: Select + options: + options: + - New + - Pending review + - Accepted + - Obsolete + - Duplicate + Task: + name: Task + fields: + AccountId: + label: Account + kind: Record + options: + labelField: AccountName + module: Account + queryFields: + - AccountName + selectType: default + ActivityDate: + label: Due Date + kind: DateTime + required: true + multi: true + CallDisposition: + label: Call Result + kind: String + CallDurationInSeconds: + label: Call Duration + kind: Number + options: + format: 0 + precision: 0 + CallObject: + label: Call Object Identifier + kind: String + CallType: + label: Call Type + kind: Select + options: + options: + - Internal + - Inbound + - Outbound + selectType: default + CaseId: + label: Case + kind: Record + options: + labelField: Subject + module: Case + queryFields: + - AccountId + - CaseNumber + - Subject + selectType: default + ContactId: + label: Contact + kind: Record + options: + labelField: RecordLabel + module: Contact + queryFields: + - RecordLabel + selectType: default + ContractId: + label: Contract + kind: Record + options: + labelField: Name + module: Contract + queryFields: + - AccountId + - Name + - ContractNumber + selectType: default + Description: + label: Comments + kind: String + options: + multiLine: true + useRichTextEditor: true + IsClosed: + label: Closed + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + IsReminderSet: + label: Reminder Set + kind: Bool + options: + falseLabel: "No" + trueLabel: "Yes" + LeadId: + label: Lead + kind: Record + options: + labelField: RecordLabel + module: Lead + queryFields: + - RecordLabel + selectType: default + OpportunityId: + label: Opportunity + kind: Record + options: + labelField: Name + module: Opportunity + queryFields: + - AccountId + - Name + selectType: default + OwnerId: + label: Assigned To + kind: User + options: + presetWithAuthenticated: false + selectType: default + Priority: + label: Priority + kind: Select + options: + options: + - High + - Normal + - Low + selectType: default + QuoteId: + label: Quote + kind: Record + options: + labelField: Name + module: Quote + queryFields: + - AccountId + - Name + - QuoteNumber + selectType: default + ReminderDateTime: + label: Reminder Date/Time + kind: DateTime + Status: + label: Status + kind: Select + options: + options: + - Open + - Completed + selectType: default + Subject: + label: Subject + kind: String + Type: + label: Type + kind: Select + options: + options: + - Call + - Email + - Send letter + - Send Quote + - Send Message + - Other + selectType: default + Vote: + name: Vote + fields: + IdeaId: + label: Idea + kind: Record + options: + labelField: Title + module: Idea + queryFields: + - Title + QuestionId: + label: Question + kind: Record + options: + labelField: Title + module: Question + queryFields: + - Title + ReplyId: + label: Reply + kind: Record + options: + labelField: Name + module: Reply + queryFields: + - Name + Score: + label: Score + kind: Number + options: + format: 0 + precision: 0 diff --git a/provision/compose/src/1200_charts.yaml b/provision/compose/src/1200_charts.yaml new file mode 100644 index 000000000..745cb7794 --- /dev/null +++ b/provision/compose/src/1200_charts.yaml @@ -0,0 +1,103 @@ +namespace: crm +charts: + LeadsByCountry: + name: Leads by country + config: + reports: + - metrics: + - backgroundColor: '#e5a83b' + beginAtZero: true + field: count + fixTooltips: true + label: Number of leads + type: bar + dimensions: + - conditions: {} + field: Country + modifier: (no grouping / buckets) + module: Lead + LeadsBySource: + name: Leads by source + config: + reports: + - metrics: + - backgroundColor: '#36d436' + beginAtZero: true + field: count + label: Number of leads + type: bar + dimensions: + - conditions: {} + field: LeadSource + modifier: (no grouping / buckets) + module: Lead + LeadsByType: + name: Leads by type + config: + reports: + - metrics: + - backgroundColor: '#63da1a' + beginAtZero: true + field: count + label: Number of leads + type: bar + dimensions: + - conditions: {} + field: Status + modifier: (no grouping / buckets) + module: Lead + LeadsPerDay: + name: Leads per day + config: + reports: + - metrics: + - aggregate: SUM + axisType: linear + backgroundColor: '#fc7507' + beginAtZero: true + field: count + fill: false + label: Leads per day + type: line + dimensions: + - conditions: {} + field: created_at + modifier: DATE + module: Lead + OpportunitiesByValue: + name: Opportunities by value + config: + reports: + - metrics: + - aggregate: SUM + backgroundColor: '#5977ff' + beginAtZero: true + field: Amount + label: Total value + type: bar + dimensions: + - conditions: {} + field: StageName + modifier: (no grouping / buckets) + module: Opportunity + QuarterlyPerformance: + name: Quarterly performance + config: + reports: + - filter: Amount > 0 + metrics: + - aggregate: SUM + axisPosition: left + axisType: linear + backgroundColor: '#009eff' + beginAtZero: true + field: Amount + fill: true + label: Amount + type: line + dimensions: + - autoSkip: true + conditions: {} + field: CloseDate + modifier: QUARTER + module: Opportunity diff --git a/provision/compose/src/1300_scripts.yaml b/provision/compose/src/1300_scripts.yaml new file mode 100644 index 000000000..783d1ec83 --- /dev/null +++ b/provision/compose/src/1300_scripts.yaml @@ -0,0 +1,1847 @@ +namespace: crm +scripts: + AccountCreateNewOpportunity: + source: |- + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + let opportunityCloseDays = defaultSettings.values.OpportunityCloseDateDays + let opportunityProbability = defaultSettings.values.OpportunityProbability + let opportunityForecaseCategory = defaultSettings.values.OpportunityForecaseCategory + let opportunityStagename = defaultSettings.values.OpportunityStagename + + //Calculate the expiration date + let m = new Date() + m.setDate(m.getDate() + parseInt(opportunityCloseDays, 10)) + let closeDate = m.getUTCFullYear() + "/" + (m.getUTCMonth() + 1) + "/" + m.getUTCDate() + " " + m.getUTCHours() + ":" + m.getUTCMinutes() + ":" + m.getUTCSeconds() + + // Find the contact we want to link the new case to (by default, the primary contact) + return Compose.findRecords(`AccountId = ${$record.recordID}`, 'Contact') + + .then(({ set, filter }) => { + + let ContactId, SuppliedName, SuppliedEmail, SuppliedPhone + + // Loop through the contacts of the account, to save the primary contact + set.forEach(r => { + + //Check if it's the primary contact + let contactIsPrimary = r.values.IsPrimary + if (contactIsPrimary === '1') { + + //Add the contact + ContactId = r.recordID + } + }) + + // Create the related opportunity + return Compose.makeRecord({ + 'OwnerId': $record.values.OwnerId, + 'LeadSource': $record.values.LeadSource, + 'Name': '(unnamed)', + 'AccountId': $record.recordID, + 'IsClosed': 'No', + 'IsWon': 'No', + 'CloseDate': closeDate, + 'Probability': opportunityProbability, + 'ForecastCategory': opportunityForecaseCategory, + 'StageName': opportunityStagename + }, 'Opportunity') + + .then(myOpportunity => { + + return Compose.saveRecord(myOpportunity) + + }).then(mySavedOpportunity => { + + //Create a new contact linked to the opportunity + return Compose.makeRecord({ + 'ContactId': ContactId, + 'OpportunityId': mySavedOpportunity.recordID, + 'IsPrimary': '1' + }, 'OpportunityContactRole') + + .then(myOpportunityContactRole => { + + return Compose.saveRecord(myOpportunityContactRole) + + .then(() => { + + // Notify current user + ComposeUI.success(`The new opportunity has been created.`) + + + }).then(() => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedOpportunity) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + CaseInsertCaseNumber: + source: |- + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Map the case number + let nextCaseNumber = defaultSettings.values.CaseNextNumber + if (typeof nextCaseNumber == "undefined" || nextCaseNumber === '' || isNaN(nextCaseNumber)) { + nextCaseNumber = 0 + } + + $record.values.CaseNumber = nextCaseNumber + let nextCaseNumberUpdated = parseInt(nextCaseNumber,10) + 1 + + // Update the config + defaultSettings.values.CaseNextNumber = nextCaseNumberUpdated + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + + console.log('Record saved, new ID', mySavedDefaultSettings.recordID) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - enabled: false + event: afterCreate + module: Case + resource: compose:record + - event: beforeCreate + module: Case + resource: compose:record + CaseInformContactOfSolution: + source: |- + // Check if the case is solved + if (!$record.values.Status === 'Closed') { + + // Get the to address + let to = $record.values.SuppliedEmail + if (!to) { + ComposeUI.warning(`There is no supplied email. Please fill in an email address in the supplied email field.`) + return + } + + // Get email body + let html = "

Solution of case: " + $record.values.CaseNumber + " - " + $record.values.Subject + "

" + html = html + "
" + html = html + "Solution Title: " + $record.values.SolutionName + "
" + html = html + "Solution Details: " + $record.values.SolutionNote + + // Send the email + return Compose.sendMail(to, `Corteza - Quote: ${$record.values.QuoteNumber} - ${$record.values.Name}`, { html: html }).then(() => { + ComposeUI.success(`The case solutoin has been sent via email.`) + }).catch(err => { + // solve the problem + console.error(err) + }) + } else { + ComposeUI.warning(`You can only inform the client of a solution when the case status is "Closed".`) + return + } + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + ContactSetRecordLabel: + source: |- + // Set the record label string + let recordLabel = '' + + // Get the first name + let firstName = $record.values.FirstName + if (!firstName) { + firstName = '' + } + + // Get the last name + let lastName = $record.values.LastName + if (!lastName) { + lastName = '' + } + + // Create the full name + let fullName = '' + if ((firstName !== '') && (lastName === '')) { + recordLabel = firstName + } + + if ((firstName === '') && (lastName !== '')) { + recordLabel = lastName + } + + if ((firstName !== '') && (lastName !== '')) { + recordLabel = firstName + ' ' + lastName + } + + // Get the company name from the account + // Check if there is a related account, to map the fields of the account + let accountId = $record.values.AccountId + if (accountId) { + return Compose.findRecordByID(accountId, 'Account').then(accountRecord => { + + // Add to the record label + recordLabel = recordLabel + ' (' + accountRecord.values.AccountName + ')' + $record.values.RecordLabel = recordLabel + + }).catch(err => { + // solve the problem + $record.values.RecordLabel = recordLabel + + console.error(err) + }) + + } + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - enabled: false + event: afterCreate + module: Contact + resource: compose:record + - enabled: false + event: afterUpdate + module: Contact + resource: compose:record + - event: beforeUpdate + module: Contact + resource: compose:record + - event: beforeCreate + module: Contact + resource: compose:record + LeadSetRecordLabel: + source: |- + // Set the record label string + let recordLabel = '' + + // Get the first name + let firstName = $record.values.FirstName + if (!firstName) { + fistName = '' + } + + // Get the last name + let lastName = $record.values.LastName + if (!lastName) { + lastName = '' + } + + // Create the full name + let fullName = '' + if ((firstName !== '') && (lastName === '')) { + recordLabel = firstName + } + + if ((firstName === '') && (lastName !== '')) { + recordLabel = lastName + } + + if ((firstName !== '') && (lastName !== '')) { + recordLabel = firstName + ' ' + lastName + } + + // Get the company name + let company = $record.values.Company + if (!company) { + company = '' + } + + // Add the company name (if there is one) + if (company !== '') { + recordLabel = recordLabel + ' (' + company + ')' + } + + // Set the label + $record.values.RecordLabel = recordLabel + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - enabled: false + event: afterCreate + module: Lead + resource: compose:record + - enabled: false + event: afterUpdate + module: Lead + resource: compose:record + - event: beforeUpdate + module: Lead + resource: compose:record + - event: beforeCreate + module: Lead + resource: compose:record + QuoteApproveQuote: + source: |- + //Check if the quote has the correct status + if ($record.values.Status !== 'In Review') { + // Inform + ComposeUI.warning(`A quote needs to have the status In Review in order to be approved.`) + return true + } + + //Change value + $record.values.Status = 'Approved' + + return Compose.saveRecord($record) + .then(mySavedRecord => { + + // Get the email of the owner + return System.findUserByID($record.createdBy).then(user => { + + // Send the mail + return Compose.sendMail( + user.email, + `Quote "${$record.values.Name}" has been approved`, + { html: `The following quote has been approved:

${$record.values.Name}` } + ) + }).then(() => { + + // Notify current user + ComposeUI.success(`The quote has been approved and the quote owner has been notified via email.`) + + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + SolutionInsertSolutionNumber: + source: |- + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Map the case number + let nextSolutionNumber = defaultSettings.values.SolutionNextNumber + if (typeof nextSolutionNumber == "undefined" || nextSolutionNumber === '' || isNaN(nextSolutionNumber)) { + nextSolutionNumber = 0 + } + $record.values.SolutionNumber = nextSolutionNumber + let nextSolutionNumberUpdated = parseInt(nextSolutionNumber,10) + 1 + + // Update the config + defaultSettings.values.SolutionNextNumber = nextSolutionNumberUpdated + + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + console.log('Record saved, new ID', mySavedDefaultSettings.recordID) + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - enabled: false + event: afterCreate + module: Solution + resource: compose:record + - event: beforeCreate + module: Solution + resource: compose:record + AccountCreateNewContract: + source: |- + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Map the case number + let ContractDefaultTime = defaultSettings.values.ContractDefaultTime + + // Get the contract number + let nextContractNumber = defaultSettings.values.ContractNextNumber + if (typeof nextContractNumber == "undefined" || nextContractNumber === '' || isNaN(nextContractNumber)) { + nextContractNumber = 0 + } + + return Compose.makeRecord({ + 'OwnerId' : $record.values.OwnerId, + 'AccountId' : $record.recordID, + 'Status' : 'Draft', + 'BillingStreet' : $record.values.BillingStreet, + 'BillingCity' : $record.values.BillingCity, + 'BillingState' : $record.values.BillingState, + 'BillingPostalCode' : $record.values.BillingPostalCode, + 'BillingCountry' : $record.values.BillingCountry, + 'ShippingStreet' : $record.values.BillingStreet, + 'ShippingCity' : $record.values.BillingCity, + 'ShippingState' : $record.values.BillingState, + 'ShippingPostalCode' : $record.values.BillingPostalCode, + 'ShippingCountry' : $record.values.BillingCountry, + 'ContractTerm' : ContractDefaultTime, + 'ContractNumber' : nextContractNumber + }, 'Contract').then(myContract => { + + // return was missing here. Set tjhe mySavedCase below + return Compose.saveRecord(myContract) + + }).then(mySavedContract => { + let nextContractNumberUpdated = parseInt(nextContractNumber,10) + 1 + + // Update the config + defaultSettings.values.ContractNextNumber = nextContractNumberUpdated + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + + console.log('Record saved, new ID', mySavedDefaultSettings.recordID) + + }).then(() => { + + // Notify current user + ComposeUI.success(`The new contract record has been created.`) + + }).then(() => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedContract) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + CaseSetStatusToWorking: + source: |- + // Update the status + $record.values.Status = 'Working' + let caseRecord = $record + + // Save the case + return Compose.makeRecord({ + 'CaseId' : caseRecord.recordID, + 'Description' : 'State set to "Working"', + 'Type' : 'State change' + }, 'CaseUpdate').then(myCaseUpdate => { + + return Compose.saveRecord(myCaseUpdate) + + }).then(myCaseUpdate => { + + return Compose.saveRecord(caseRecord) + + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + CaseSetStatusToClosed: + source: |- + // Check if the case is already closed + if ($record.values.Status === 'Closed') { + // Case is closed already. Exit + ComposeUI.success(`This case is already closed.`) + return true + } + + // Check if there is a solution + let SolutionName = $record.values.SolutionName + let solutionRecord = $record.values.SolutionId + + // If there is no solution, show that it's not possible to close the case + if ((!SolutionName) && (!solutionRecord)) { + ComposeUI.warning(`Unable to close the case. Please add a solution name or select an existing solution before closing the case.`) + return true + } + + // Update the status + $record.values.IsClosed = true + var m = new Date() + var dateString = m.getUTCFullYear() + "/" + (m.getUTCMonth() + 1) + "/" + m.getUTCDate() + " " + m.getUTCHours() + ":" + m.getUTCMinutes() + ":" + m.getUTCSeconds() + $record.values.ClosedDate = dateString + $record.values.Status = 'Closed' + + return Compose.saveRecord($record) + .then(mySavedRecord => { + + // Create the CaseUpdate record + return Compose.makeRecord({ + 'Description': 'State set to "Closed', + 'Type': 'State change', + 'CaseId': $record.recordID + }, 'CaseUpdate') + + .then(myCaseUpdate => { + + return Compose.saveRecord(myCaseUpdate) + + }).then(mySavedCaseUpdate => { + + // Check if a solution record has been selected + if (solutionRecord) { + + // If there is a solution record, map the values in the case + return Compose.findRecordByID(solutionRecord, 'Solution').then(solution => { + $record.values.SolutionName = solution.values.SolutionName + $record.values.SolutionNote = solution.values.SolutionNote + $record.values.SolutionFile = solution.values.File + + return Compose.saveRecord($record) + }).catch(err => { + + // solve the problem + console.error(err) + }) + } else { + + // If there is no solution record, check if the value "SubmitAsSolution" is checked. If so, save the solution as a Solution record + if ($record.values.SubmitAsSolution) { + + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Map the solution number + let nextSolutionNumber = defaultSettings.values.SolutionNextNumber + if (typeof nextSolutionNumber == "undefined" || nextSolutionNumber === '' || isNaN(nextSolutionNumber)) { + nextSolutionNumber = 0 + } + + // Create the Solution record + return Compose.makeRecord({ + 'SolutionName': $record.values.SolutionName, + 'SolutionNote': $record.values.SolutionNote, + 'File': $record.values.SolutionFile, + 'Status': 'New', + 'IsPublished': '1', + 'CaseId': $record.recordID, + 'SolutionNumber': nextSolutionNumber, + 'ProductId': $record.values.ProductId + }, 'Solution') + + .then(mySolution => { + + return Compose.saveRecord(mySolution) + + }).then(mySavedSolution => { + + // Save the solution record in the case record + $record.values.SolutionId = mySavedSolution.recordID + return Compose.saveRecord($record) + + }).then(mySavedSolution => { + + let nextSolutionNumberUpdated = parseInt(nextSolutionNumber, 10) + 1 + + // Update the config + defaultSettings.values.SolutionNextNumber = nextSolutionNumberUpdated + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + + console.log('Record saved, ID', mySavedDefaultSettings.recordID) + + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + } + } + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + AccountCreateNewCase: + source: |- + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Map the case number + let nextCaseNumber = defaultSettings.values.CaseNextNumber + if (typeof nextCaseNumber == "undefined" || nextCaseNumber === '' || isNaN(nextCaseNumber)) { + nextCaseNumber = 0 + } + + // Find the contact we want to link the new case to (by default, the primary contact) + return Compose.findRecords(`AccountId = ${$record.recordID}`, 'Contact').then(({ set, filter }) => { + + let ContactId, SuppliedName, SuppliedEmail, SuppliedPhone + + // Loop through the contacts of the account, to save the primary contact + set.forEach(r => { + + //Check if it's the primary contact + let contactIsPrimary = r.values.IsPrimary + console.log(contactIsPrimary) + if (contactIsPrimary === '1') { + + //Add the contact + ContactId = r.recordID + SuppliedName = r.values.FirstName + ' ' + r.values.LastName + SuppliedEmail = r.values.Email + SuppliedPhone = r.values.Phone + } + }) + + return Compose.makeRecord({ + 'OwnerId' : $record.values.OwnerId, + 'Subject' : '(no subject)', + 'ContactId' : ContactId, + 'AccountId' : $record.recordID, + 'Status' : 'New', + 'Priority' : 'Low', + 'SuppliedName' : SuppliedName, + 'SuppliedEmail' : SuppliedEmail, + 'SuppliedPhone' : SuppliedPhone, + 'CaseNumber' : nextCaseNumber + }, 'Case').then(myCase => { + + Compose.saveRecord(myCase) + + }).then(mySavedCase => { + let nextCaseNumberUpdated = parseInt(nextCaseNumber,10) + 1 + + // Update the config + defaultSettings.values.CaseNextNumber = nextCaseNumberUpdated + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + + console.log('Record saved, new ID', mySavedDefaultSettings.recordID) + + }).then(({ mySavedCase }) => { + + // Notify current user + ComposeUI.success(`The new case has been created.`) + + + }).then(({ mySavedCase }) => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedCase) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + ContactCreateNewCase: + source: |- + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Map the case number + let nextCaseNumber = defaultSettings.values.CaseNextNumber + if (typeof nextCaseNumber == "undefined" || nextCaseNumber === '' || isNaN(nextCaseNumber)) { + nextCaseNumber = 0 + } + + return Compose.makeRecord({ + 'OwnerId' : $record.values.OwnerId, + 'Subject' : '(no subject)', + 'ContactId' : $record.recordID, + 'AccountId' : $record.values.AccountId, + 'Status' : 'New', + 'Priority' : 'Low', + 'SuppliedName' : $record.values.FirstName + ' ' + $record.values.LastName, + 'SuppliedEmail' : $record.values.Email, + 'SuppliedPhone' : $record.values.Phone, + 'CaseNumber' : nextCaseNumber + }, 'Case').then(myCase => { + + // return was missing here. Set tjhe mySavedCase below + return Compose.saveRecord(myCase) + + }).then(mySavedCase => { + let nextCaseNumberUpdated = parseInt(nextCaseNumber, 10) + 1 + + // Update the config + defaultSettings.values.CaseNextNumber = nextCaseNumberUpdated + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + + console.log('Record saved, new ID', mySavedDefaultSettings.recordID) + // then(({ mySavedCase }) wasn't needed, since it is already defined + // for the entire block, starting at line 27 + }).then(() => { + + // Notify current user + ComposeUI.success(`The new case has been created.`) + + }).then(() => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedCase) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + LeadConvertLeadIntoAccount: + source: |- + if ($record.values.Status === 'Converted') { + // Lead already converted. Inform user and exit + ComposeUI.warning('This lead is already converted.') + return true + } + + // create new record of type/module Account and copy all values + return Compose.makeRecord({ + 'BillingStreet' : $record.values.Street, + 'BillingCity' : $record.values.City, + 'BillingState' : $record.values.State, + 'BillingPostalCode' : $record.values.PostalCode, + 'BillingCountry' : $record.values.Country, + 'AnnualRevenue' : $record.values.AnnualRevenue, + 'AccountName' : $record.values.Company, + 'Description' : $record.values.Description, + 'Fax' : $record.values.Fax, + 'Industry' : $record.values.Industry, + 'OwnerId' : $record.values.OwnerId, + 'AccountSource' : $record.values.LeadSource, + 'Phone' : $record.values.Phone, + 'NumberOfEmployees' : $record.values.NumberOfEmployees, + 'Rating' : $record.values.Rating, + 'Website' : $record.values.Website, + 'Twitter' : $record.values.Twitter, + 'Facebook' : $record.values.Facebook, + 'LinkedIn' : $record.values.LinkedIn + }, 'Account').then(myAccount => { + + return Compose.saveRecord(myAccount) + + }).then(mySavedAccount => { + + // Create the related contact + return Compose.makeRecord({ + 'MailingStreet' : $record.values.Street, + 'MailingCity' : $record.values.City, + 'MailingState' : $record.values.State, + 'MailingPostalCode' : $record.values.PostalCode, + 'MailingCountry' : $record.values.Country, + 'Description' : $record.values.Description, + 'DoNotCall' : $record.values.DoNotCall, + 'Email' : $record.values.Email, + 'HasOptedOutOfEmail' : $record.values.HasOptedOutOfEmail, + 'Fax' : $record.values.Fax, + 'HasOptedOutOfFax' : $record.values.HasOptedOutOfFax, + 'OwnerId' : $record.values.OwnerId, + 'LeadSource' : $record.values.LeadSource, + 'Website' : $record.values.Website, + 'Twitter' : $record.values.Twitter, + 'Facebook' : $record.values.Facebook, + 'LinkedIn' : $record.values.LinkedIn, + 'Salutation' : $record.values.Salutation, + 'FirstName' : $record.values.FirstName, + 'LastName' : $record.values.LastName, + 'MobilePhone' : $record.values.MobilePhone, + 'Phone' : $record.values.Phone, + 'Title' : $record.values.Title, + 'IsPrimary' : '1', + 'AccountId' : mySavedAccount.recordID + }, 'Contact').then(myContact => { + + return Compose.saveRecord(myContact) + + }).then(updatedAccount => { + + //Update the lead record + $record.values.Status = 'Converted' + $record.values.IsConverted = 'Yes' + $record.values.ConvertedAccountId = mySavedAccount.recordID + $record.values.ConvertedContactId = mySavedAccount.recordID + $record.values.ConvertedDate = mySavedAccount.createdAt + + return Compose.saveRecord($record) + + }).then(leadUser => { + + return System.findUserByID($record.values.OwnerId).then(user => { + + // Notifies the owner that a new account was created and assigned to him + Compose.sendRecordToMail( + user.email, + 'Lead ' + $record.values.FirstName + ' ' + $record.values.LastName + ' from ' + $record.values.Company + ' has been converted', + { + header: '

The following lead has been converted:

' + }, + mySavedAccount + ) + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).then(({ mySavedAccount }) => { + + // Notify current user + ComposeUI.success(`The lead has been converted.`) + + + }).then(({ mySavedAccount }) => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedAccount) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + LeadConvertLeadToAccountAndOpportunity: + source: |- + if ($record.values.Status === 'Converted') { + // Lead already converted. Inform user and exit + ComposeUI.warning('This lead is already converted.') + return true + } + + // create new record of type/module Account and copy all values + return Compose.makeRecord({ + 'BillingStreet' : $record.values.Street, + 'BillingCity' : $record.values.City, + 'BillingState' : $record.values.State, + 'BillingPostalCode' : $record.values.PostalCode, + 'BillingCountry' : $record.values.Country, + 'AnnualRevenue' : $record.values.AnnualRevenue, + 'AccountName' : $record.values.Company, + 'Description' : $record.values.Description, + 'Fax' : $record.values.Fax, + 'Industry' : $record.values.Industry, + 'OwnerId' : $record.values.OwnerId, + 'AccountSource' : $record.values.LeadSource, + 'Phone' : $record.values.Phone, + 'NumberOfEmployees' : $record.values.NumberOfEmployees, + 'Rating' : $record.values.Rating, + 'Website' : $record.values.Website, + 'Twitter' : $record.values.Twitter, + 'Facebook' : $record.values.Facebook, + 'LinkedIn' : $record.values.LinkedIn + }, 'Account').then(myAccount => { + + return Compose.saveRecord(myAccount) + + }).then(mySavedAccount => { + + // Create the related contact + return Compose.makeRecord({ + 'MailingStreet' : $record.values.Street, + 'MailingCity' : $record.values.City, + 'MailingState' : $record.values.State, + 'MailingPostalCode' : $record.values.PostalCode, + 'MailingCountry' : $record.values.Country, + 'Description' : $record.values.Description, + 'DoNotCall' : $record.values.DoNotCall, + 'Email' : $record.values.Email, + 'HasOptedOutOfEmail' : $record.values.HasOptedOutOfEmail, + 'Fax' : $record.values.Fax, + 'HasOptedOutOfFax' : $record.values.HasOptedOutOfFax, + 'OwnerId' : $record.values.OwnerId, + 'LeadSource' : $record.values.LeadSource, + 'Website' : $record.values.Website, + 'Twitter' : $record.values.Twitter, + 'Facebook' : $record.values.Facebook, + 'LinkedIn' : $record.values.LinkedIn, + 'Salutation' : $record.values.Salutation, + 'FirstName' : $record.values.FirstName, + 'LastName' : $record.values.LastName, + 'MobilePhone' : $record.values.MobilePhone, + 'Phone' : $record.values.Phone, + 'Title' : $record.values.Title, + 'IsPrimary' : '1', + 'AccountId' : mySavedAccount.recordID + }, 'Contact').then(mySavedContact => { + + return Compose.saveRecord(mySavedContact).then(mySavedContact => { + + // First get the default values + //Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + let opportunityCloseDays = defaultSettings.values.OpportunityCloseDateDays + let opportunityProbability = defaultSettings.values.OpportunityProbability + let opportunityForecaseCategory = defaultSettings.values.OpportunityForecaseCategory + let opportunityStagename = defaultSettings.values.OpportunityStagename + + //Calculate the expiration date + let m = new Date() + m.setDate(m.getDate() + parseInt(opportunityCloseDays,10)) + let closeDate = m.getUTCFullYear() +"/"+ (m.getUTCMonth()+1) +"/"+ m.getUTCDate() + " " + m.getUTCHours() + ":" + m.getUTCMinutes() + ":" + m.getUTCSeconds() + + // Create the related opportunity + return Compose.makeRecord({ + 'Description' : $record.values.Description, + 'OwnerId' : $record.values.OwnerId, + 'LeadSource' : $record.values.LeadSource, + 'Name' : '(unnamed)', + 'AccountId' : mySavedAccount.recordID, + 'IsClosed' : 'No', + 'IsWon' : 'No', + 'CloseDate' : closeDate, + 'Probability' : opportunityProbability, + 'ForecastCategory' : opportunityForecaseCategory, + 'StageName' : opportunityStagename + }, 'Opportunity').then(myOpportunity => { + return Compose.saveRecord(myOpportunity) + + }).then(mySavedOpportunity => { + + //Create a new contact linked to the opportunity + return Compose.makeRecord({ + 'ContactId' : mySavedContact.recordID, + 'OpportunityId' : mySavedOpportunity.recordID, + 'IsPrimary' : '1' + }, 'OpportunityContactRole').then(myOpportunityContactRole => { + return Compose.saveRecord(myOpportunityContactRole) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + + }).then(updatedAccount => { + + //Update the lead record + $record.values.Status = 'Converted' + $record.values.IsConverted = 'Yes' + $record.values.ConvertedAccountId = mySavedAccount.recordID + $record.values.ConvertedContactId = mySavedAccount.recordID + $record.values.ConvertedDate = mySavedAccount.createdAt + + return Compose.saveRecord($record) + + }).then(leadUser => { + + return System.findUserByID($record.values.OwnerId).then(user => { + + // Notifies the owner that a new account was created and assigned to him + Compose.sendRecordToMail( + user.email, + 'Lead ' + $record.values.FirstName + ' ' + $record.values.LastName + ' from ' + $record.values.Company + ' has been converted', + { + header: '

The following lead has been converted:

' + }, + mySavedAccount + ) + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).then(({ mySavedAccount }) => { + + // Notify current user + ComposeUI.success(`The lead has been converted.`) + + + }).then(({ mySavedAccount }) => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedAccount) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + QuoteSendQuoteToCustomEmail: + source: |- + // Get the to address + let to = prompt("Please enter an email to send this quote to:") + if (to === null || to === "") { + ComposeUI.warning(`Please enter an email to send this quote to .`) + return + } + + let lineitems = '' + Compose.findRecords(`QuoteId = ${$record.recordID}`, 'QuoteLineItem') + + .then(({ set, filter }) => { + + set.forEach(lineitem => { + + lineitems = lineitems + "Price: " + lineitem.values.UnitPrice + "
" + lineitems = lineitems + "Quantity: " + lineitem.values.Quantity + "
" + lineitems = lineitems + "Subtotal: " + lineitem.values.Subtotal + "
" + lineitems = lineitems + "Discount: " + lineitem.values.Discount + "
" + lineitems = lineitems + "Total Price: " + lineitem.values.TotalPrice + "
" + lineitems = lineitems + "-------------------------
" + }) + return lineitems + }).then(lineitems => { + + + // Get email body + let html = "

Details of Quote: " + $record.values.QuoteNumber + " - " + $record.values.Name + "

" + html = html + "
" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "
" + html = html + "Quote Information
" + html = html + "
" + html = html + "Quote Number: " + $record.values.QuoteNumber + "
" + html = html + "Quote Name: " + $record.values.Name + "
" + html = html + "Expiration Date: " + $record.values.ExpirationDate + "
" + html = html + "Description: " + $record.values.Description + html = html + "
" + html = html + "Primary contact data
" + html = html + "
" + html = html + "Contact Name: " + $record.values.ContactId + "
" + html = html + "Email: " + $record.values.Email + "
" + html = html + "Phone: " + $record.values.Phone + html = html + "
" + html = html + "Totals
" + html = html + "
" + html = html + "Subtotal: " + $record.values.Subtotal + "
" + html = html + "Additional Discount: " + $record.values.Discount + "
" + html = html + "Shipping and Handling: " + $record.values.ShippingHandling + "
" + html = html + "Total Price: " + $record.values.TotalPrice + "
" + html = html + "Tax: " + $record.values.Tax + "
" + html = html + "Grand Total: " + $record.values.GrandTotal + html = html + "
" + html = html + "Products
" + html = html + "
" + html = html + lineitems + html = html + "
" + html = html + "Bill To
" + html = html + "
" + html = html + "Bill to Name: " + $record.values.BillingName + "
" + html = html + "Bill to Street: " + $record.values.BillingStreet + "
" + html = html + "Bill to City: " + $record.values.BillingCity + "
" + html = html + "Bill to State: " + $record.values.BillingState + "
" + html = html + "Bill to Postal Code: " + $record.values.BillingPostalCode + "
" + html = html + "Bill to Country: " + $record.values.BillingCountry + html = html + "
" + html = html + "Quote To
" + html = html + "
" + html = html + "Quote to Name: " + $record.values.QuoteToName + "
" + html = html + "Quote to Street: " + $record.values.QuoteToStreet + "
" + html = html + "Quote to City: " + $record.values.QuoteToCity + "
" + html = html + "Quote to State: " + $record.values.QuoteToState + "
" + html = html + "Quote to Postal Code: " + $record.values.QuoteToPostalCode + "
" + html = html + "Quote to Country: " + $record.values.QuoteToCountry + html = html + "
" + html = html + "Ship To
" + html = html + "
" + html = html + "Ship to Name: " + $record.values.ShippingName + "
" + html = html + "Ship to Street: " + $record.values.ShippingStreet + "
" + html = html + "Ship to City: " + $record.values.ShippingCity + "
" + html = html + "Ship to State: " + $record.values.ShippingState + "
" + html = html + "Ship to Postal Code: " + $record.values.ShippingPostalCode + "
" + html = html + "Ship to Country: " + $record.values.ShippingCountry + html = html + "
" + + // Send the email + return Compose.sendMail(to, `Corteza - Quote: ${$record.values.QuoteNumber} - ${$record.values.Name}`, { html: html }).then(() => { + ComposeUI.success(`The quote has been sent via email.`) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + QuoteSendQuoteToPrimaryContact: + source: |- + // Get the to address + let to = $record.values.Email + if (!to) { + ComposeUI.warning(`There is no email linked to the quote. Please fill in an email address in the "Primary contact data" block.`) + return + } + + let lineitems = '' + Compose.findRecords(`QuoteId = ${$record.recordID}`, 'QuoteLineItem') + + .then(({ set, filter }) => { + + set.forEach(lineitem => { + + lineitems = lineitems + "Price: " + lineitem.values.UnitPrice + "
" + lineitems = lineitems + "Quantity: " + lineitem.values.Quantity + "
" + lineitems = lineitems + "Subtotal: " + lineitem.values.Subtotal + "
" + lineitems = lineitems + "Discount: " + lineitem.values.Discount + "
" + lineitems = lineitems + "Total Price: " + lineitem.values.TotalPrice + "
" + lineitems = lineitems + "-------------------------
" + }) + return lineitems + }).then(lineitems => { + + + // Get email body + let html = "

Details of Quote: " + $record.values.QuoteNumber + " - " + $record.values.Name + "

" + html = html + "
" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "" + html = html + "
" + html = html + "Quote Information
" + html = html + "
" + html = html + "Quote Number: " + $record.values.QuoteNumber + "
" + html = html + "Quote Name: " + $record.values.Name + "
" + html = html + "Expiration Date: " + $record.values.ExpirationDate + "
" + html = html + "Description: " + $record.values.Description + html = html + "
" + html = html + "Primary contact data
" + html = html + "
" + html = html + "Contact Name: " + $record.values.ContactId + "
" + html = html + "Email: " + $record.values.Email + "
" + html = html + "Phone: " + $record.values.Phone + html = html + "
" + html = html + "Totals
" + html = html + "
" + html = html + "Subtotal: " + $record.values.Subtotal + "
" + html = html + "Additional Discount: " + $record.values.Discount + "
" + html = html + "Shipping and Handling: " + $record.values.ShippingHandling + "
" + html = html + "Total Price: " + $record.values.TotalPrice + "
" + html = html + "Tax: " + $record.values.Tax + "
" + html = html + "Grand Total: " + $record.values.GrandTotal + html = html + "
" + html = html + "Products
" + html = html + "
" + html = html + lineitems + html = html + "
" + html = html + "Bill To
" + html = html + "
" + html = html + "Bill to Name: " + $record.values.BillingName + "
" + html = html + "Bill to Street: " + $record.values.BillingStreet + "
" + html = html + "Bill to City: " + $record.values.BillingCity + "
" + html = html + "Bill to State: " + $record.values.BillingState + "
" + html = html + "Bill to Postal Code: " + $record.values.BillingPostalCode + "
" + html = html + "Bill to Country: " + $record.values.BillingCountry + html = html + "
" + html = html + "Quote To
" + html = html + "
" + html = html + "Quote to Name: " + $record.values.QuoteToName + "
" + html = html + "Quote to Street: " + $record.values.QuoteToStreet + "
" + html = html + "Quote to City: " + $record.values.QuoteToCity + "
" + html = html + "Quote to State: " + $record.values.QuoteToState + "
" + html = html + "Quote to Postal Code: " + $record.values.QuoteToPostalCode + "
" + html = html + "Quote to Country: " + $record.values.QuoteToCountry + html = html + "
" + html = html + "Ship To
" + html = html + "
" + html = html + "Ship to Name: " + $record.values.ShippingName + "
" + html = html + "Ship to Street: " + $record.values.ShippingStreet + "
" + html = html + "Ship to City: " + $record.values.ShippingCity + "
" + html = html + "Ship to State: " + $record.values.ShippingState + "
" + html = html + "Ship to Postal Code: " + $record.values.ShippingPostalCode + "
" + html = html + "Ship to Country: " + $record.values.ShippingCountry + html = html + "
" + + // Send the email + return Compose.sendMail(to, `Corteza - Quote: ${$record.values.QuoteNumber} - ${$record.values.Name}`, { html: html }).then(() => { + ComposeUI.success(`The quote has been sent via email.`) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + QuoteSubmitQuoteForApproval: + source: |- + // Check if it can be reviewed + if ($record.values.Status !== 'Draft' && $record.values.Status !== 'Needs Review') { + crust.notify.ui.alert.warning('A quote needs to have the status Draft or Needs Review in order to be sent for approval') + return true + } + + // Set the status + $record.values.Status = 'In Review' + + // Saves the quote and inform + return Compose.saveRecord($record) + .then(mySavedRecord => { + + // Get the email of the owner + return System.findUserByID($record.createdBy).then(user => { + + // Send the mail + return Compose.sendMail( + user.email, //Change this to the email address of the person that needs to approve the quotes + `Quote "${$record.values.Name}" needs approval`, + { html: `The following quote needs approval:

${$record.values.Name}` } + ) + }).then(() => { + + // Notify current user + ComposeUI.success(`The quote has been sent for approval.`) + + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + OpportunityGenerateNewQuote: + source: |- + // Check if there is a related account, to map the fields of the account + if (!$record.values.AccountId) { + + // Exit when there is no account related to the opportunity. + ComposeUI.warning('Please link the opportunity to an account before generating a quote') + return + } + + let quoteContactId + let quoteEmail + let quotePhone + let quoteFax + let quoteAccountId + let quoteBillingStreet + let quoteBillingCity + let quoteBillingState + let quoteBillingPostalCode + let quoteBillingCountry + let quoteBillingName + let quoteToStreet + let quoteToCity + let quoteToState + let quoteToPostalCode + let quoteToCountry + let quoteToName + let quoteShippingStreet + let quoteShippingCity + let quoteShippingState + let quoteShippingPostalCode + let quoteShippingCountry + let quoteShippingName + let quoteExpirationDate + let quoteNumber + + // Get the primary contact for the quote + return Compose.findRecords(`OpportunityId = ${$record.recordID}`, 'OpportunityContactRole') + + .then(({ set, filter }) => { + + let primary_contact + + if (set.length === 1) { + + // Get the contact + primary_contact = set[0] + + } else { + + // Loop through the contacts of the account, to save the primary contact + set.forEach(r => { + + // Check if it's the primary contact + let contactIsPrimary = r.values.IsPrimary + if (contactIsPrimary === '1') { + + // Add the contact + primary_contact = r + } + }) + } + return primary_contact + + }).then(primary_contact => { + + // If we have the primary contact, continue to add it to the quote. Else, skip this block + if (primary_contact) { + + // Get the contact data + return Compose.findRecordByID(primary_contact.values.ContactId, 'Contact').then(contact => { + + quoteContactId = contact.recordID + quoteEmail = contact.values.Email + quotePhone = contact.values.Phone + quoteFax = contact.values.Fax + + }).catch(err => { + // solve the problem + console.error(err) + }) + } + + }).then(mySavedQuote => { + + // Get the related account + return Compose.findRecordByID($record.values.AccountId, 'Account').then(account => { + + quoteAccountId = account.recordID, + quoteBillingStreet = account.values.BillingStreet, + quoteBillingCity = account.values.BillingCity, + quoteBillingState = account.values.BillingState, + quoteBillingPostalCode = account.values.BillingPostalCode, + quoteBillingCountry = account.values.BillingCountry, + quoteBillingName = account.values.AccountName, + quoteToStreet = account.values.BillingStreet, + quoteToCity = account.values.BillingCity, + quoteToState = account.values.BillingState, + quoteToPostalCode = account.values.BillingPostalCode, + quoteToCountry = account.values.BillingCountry, + quoteToName = account.values.AccountName, + quoteShippingStreet = account.values.BillingStreet, + quoteShippingCity = account.values.BillingCity, + quoteShippingState = account.values.BillingState, + quoteShippingPostalCode = account.values.BillingPostalCode, + quoteShippingCountry = account.values.BillingCountry, + quoteShippingName = account.values.AccountName + + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).then(mySavedQuote => { + + // Get the default settings + return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => { + + // Get the expiration date + let quoteExpirationDays = defaultSettings.values.QuoteExpirationDays + + // Calculate the expiration date + let m = new Date() + m.setDate(m.getDate() + parseInt(quoteExpirationDays, 10)) + let expirationDate = m.getUTCFullYear() + "/" + (m.getUTCMonth() + 1) + "/" + m.getUTCDate() + " " + m.getUTCHours() + ":" + m.getUTCMinutes() + ":" + m.getUTCSeconds() + + // Save the date + quoteExpirationDate = expirationDate + + // Map the quote number + let nextQuoteNumber = defaultSettings.values.QuoteNextNumber + if (typeof nextQuoteNumber == "undefined" || nextQuoteNumber === '' || isNaN(nextQuoteNumber)) { + nextQuoteNumber = 0 + } + quoteNumber = nextQuoteNumber + let nextQuoteNumberUpdated = parseInt(nextQuoteNumber, 10) + 1 + + // Update the config + defaultSettings.values.QuoteNextNumber = nextQuoteNumberUpdated + return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => { + + console.log('Record saved, ID', mySavedDefaultSettings.recordID) + + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).then(mySavedQuote => { + + // Create a new quote record for the opportunity + return Compose.makeRecord({ + 'ShippingHandling': 0, + 'Status': 'Draft', + 'Discount': 0, + 'Tax': 0, + 'OpportunityId': $record.recordID, + 'GrandTotal': $record.values.Amount, + 'PricebookId': $record.values.PricebookId, + 'Name': $record.values.Name, + 'Subtotal': $record.values.Amount, + 'TotalPrice': $record.values.Amount, + 'ContactId': quoteContactId, + 'Email': quoteEmail, + 'Phone': quotePhone, + 'Fax': quoteFax, + 'AccountId': quoteAccountId, + 'BillingStreet': quoteBillingStreet, + 'BillingCity': quoteBillingCity, + 'BillingState': quoteBillingState, + 'BillingPostalCode': quoteBillingPostalCode, + 'BillingCountry': quoteBillingCountry, + 'BillingName': quoteBillingName, + 'QuoteToStreet': quoteToStreet, + 'QuoteToCity': quoteToCity, + 'QuoteToState': quoteToState, + 'QuoteToPostalCode': quoteToPostalCode, + 'QuoteToCountry': quoteToCountry, + 'QuoteToName': quoteToName, + 'ShippingStreet': quoteShippingStreet, + 'ShippingCity': quoteShippingCity, + 'ShippingState': quoteShippingState, + 'ShippingPostalCode': quoteShippingPostalCode, + 'ShippingCountry': quoteShippingCountry, + 'ShippingName': quoteShippingName, + 'ExpirationDate': quoteExpirationDate, + 'QuoteNumber': quoteNumber + }, 'Quote') + + .then(myQuote => { + + return Compose.saveRecord(myQuote) + + .then(mySavedQuote => { + + // Get the list of products from the opportunity to the quote + return Compose.findRecords(`OpportunityId = ${$record.recordID}`, 'OpportunityLineItem') + + .then(({ set, filter }) => { + + // Loop through the lineitems related to the opportunity + set.forEach(r => { + + // Create a new contact linked to the opportunity + return Compose.makeRecord({ + 'Discount': r.values.Discount, + 'Description': r.values.Description, + 'ListPrice': r.values.ListPrice, + 'PricebookEntryId': r.values.PricebookEntryId, + 'ProductId': r.values.ProductId, + 'ProductCode': r.values.ProductCode, + 'Quantity': r.values.Quantity, + 'UnitPrice': r.values.UnitPrice, + 'Subtotal': r.values.Subtotal, + 'TotalPrice': r.values.TotalPrice, + 'QuoteId': mySavedQuote.recordID + }, 'QuoteLineItem') + + .then(myQuoteLineItem => { + + return Compose.saveRecord(myQuoteLineItem) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }) + }) + + }).then(() => { + + // Notify current user + ComposeUI.success(`The new quote has been created.`) + + + }).then(() => { + + // Go to the record + ComposeUI.gotoRecordEditor(mySavedQuote) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record + QuoteUpdateTotalPrice: + source: |- + // Get the subtotal + let subtotal = parseFloat($record.values.Subtotal) + + // Apply additional quote discount + let discount = $record.values.Discount + if (!discount || discount === '' || isNaN(discount)) { + discount = 0 + } + let totalPrice = subtotal - parseFloat(discount) + + // Calculate if it's not below 0 + if (totalPrice < 0) { + totalPrice = 0 + } + + // Apply shipping + let shippingHandling = $record.values.ShippingHandling + if (!shippingHandling || shippingHandling === '' || isNaN(shippingHandling)) { + shippingHandling = 0 + } + totalPrice = totalPrice + parseFloat(shippingHandling) + + // Add totalPrice to the record + $record.values.TotalPrice = totalPrice + + // Apply taxes + let tax = $record.values.Tax + if (!shippingHandling || shippingHandling === '' || isNaN(shippingHandling)) { + // No tax, so don't do anything + $record.values.GrandTotal = totalPrice + } else { + if (tax > 0) { + // Apply tax + let taxpercent = parseFloat(tax / 100); + $record.values.GrandTotal = totalPrice * (1 + taxpercent) + } + } + async: false + runInUA: false + critical: true + enabled: true + timeout: 0 + triggers: + - event: afterUpdate + module: Quote + resource: compose:record + OpportunityApplyPriceBook: + source: |- + // Get the current price book + let pricebookId = $record.values.PricebookId + + // Check if there is a price book. If there isn't one, find the standard one + if (!pricebookId) { + + //If there is no price book selected, get the default price book. + return Compose.findRecords('IsActive = 1', 'Pricebook') + + .then(({ set, filter }) => { + + if (set.length === 0) { + + // return that there are no Price books in the CRM + ComposeUI.warning(`There are no active price books configured in the CRM. Please insert an active price book in the Price book module.`) + + } else { + + // Loop through the price books, to find the standard one + set.forEach(r => { + + //Check if the price book is the standard one + if (r.values.IsStandard === '1') { + + //Get the price book id + pricebookId = r.recordID + } + }) + + if (pricebookId) { + + //Save the price book in the opportunity + $record.values.PricebookId = pricebookId + + // Save the price book in the opportunity + return Compose.saveRecord($record) + } + } + + + }).catch(err => { + // solve the problem + console.error(err) + }) + } + + // Check if a price book is selected or if a standard price book has been found. If not, exit. + if (!pricebookId) { + ComposeUI.warning(`Please select a Price book for this opportunity first.`) + return + } + + //Set the total amount of the opportunity + let amount = 0 + + // Find all opportunity lineitems + return Compose.findRecords(`OpportunityId = ${$record.recordID}`, 'OpportunityLineItem') + + .then(({ set, filter }) => { + + set.forEach(lineitem => { + + //Set the default values + let quantity = lineitem.values.Quantity + let discount = lineitem.values.Discount + let listprice = 0 + let unitprice = 0 + let subtotal = 0 + let totalprice = 0 + + // Get the product + return Compose.findRecordByID(lineitem.values.ProductId, 'Product').then(product => { + + // Set the product name and code + lineitem.values.Name = product.values.Name + lineitem.values.ProductCode = product.values.ProductCode + + }).then(product => { + + // Get the right price from the selected price book + return Compose.findRecords(`PricebookId = ${pricebookId} AND ProductId = ${lineitem.values.ProductId}`, 'PricebookEntry') + + .then(({ set, filter }) => { + + if (set.length > 0) { + + let pricebookEntry = set[0] + + // Get the list price + listprice = pricebookEntry.values.UnitPrice + + // Update unitprice only when the value is empty + unitprice = lineitem.values.UnitPrice + if (!unitprice || unitprice === '' || isNaN(unitprice)) { + unitprice = listprice + } + + // Calculate the totals + if (!quantity || quantity === '' || isNaN(quantity)) { + quantity = 0 + } + subtotal = unitprice * quantity + + // Calculate the total + if (!discount || discount === '' || isNaN(discount)) { + totalprice = subtotal + } else { + totalprice = subtotal - discount + } + + // Update it in the listitem record + lineitem.values.ListPrice = listprice + lineitem.values.UnitPrice = unitprice + lineitem.values.Subtotal = subtotal + lineitem.values.TotalPrice = totalprice + + //Add the total price to the amount of the opportunity + amount = amount + totalprice + + // Save the lineitem + return Compose.saveRecord(lineitem) + } + }).then(mySolution => { + + //Save the opportunity record + $record.values.Amount = amount + return Compose.saveRecord($record) + }).catch(err => { + // solve the problem + console.error(err) + }) + + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + }).catch(err => { + // solve the problem + console.error(err) + }) + async: false + runInUA: true + critical: true + enabled: true + timeout: 0 + triggers: + - event: manual + resource: compose:record diff --git a/provision/compose/src/1400_pages.yaml b/provision/compose/src/1400_pages.yaml new file mode 100644 index 000000000..ad500e52b --- /dev/null +++ b/provision/compose/src/1400_pages.yaml @@ -0,0 +1,2363 @@ +namespace: crm +pages: + - handle: Home + title: Home + blocks: + - title: New Leads + options: + fields: + - name: FirstName + - name: LastName + - name: Email + - name: LeadSource + - name: Status + module: Lead + page: RecordPageForModuleLead + perPage: 5 + prefilter: Status = 'New' + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 14, 6, 9] + - title: Leads by Type + options: + chart: LeadsByType + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [9, 0, 3, 7] + - title: Open Opportunities + options: + fields: + - name: Name + - name: AccountId + - name: Amount + - name: StageName + - name: NextStep + module: Opportunity + page: RecordPageForModuleOpportunity + perPage: 3 + prefilter: StageName != 'Closed Won' AND StageName != 'Closed Lost' + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [6, 14, 6, 9] + - title: Leads by Country + options: + chart: LeadsByCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [6, 0, 3, 7] + - title: Leads per day + options: + chart: LeadsPerDay + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [6, 7, 3, 7] + - title: My Calendar + options: + feeds: + - endField: EndDateTime + module: Event + startField: ActivityDate + titleField: Subject + - endField: null + module: Task + startField: ActivityDate + titleField: Subject + header: + views: + - agendaWeek + - agendaMonth + - agendaDay + - month + - listMonth + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Calendar + xywh: [0, 0, 6, 14] + - title: Opportunities + options: + chart: OpportunitiesByValue + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [9, 7, 3, 7] + visible: true + - handle: Accounts + title: Accounts + blocks: + - title: All accounts + options: + fields: + - name: AccountName + - name: Type + - name: Rating + - name: Industry + - name: AccountSource + - name: OwnerId + - name: createdAt + module: Account + page: RecordPageForModuleAccount + perPage: 20 + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 0, 12, 19] + pages: + - handle: RecordPageForModuleAccount + module: Account + title: Record page for module "Account" + blocks: + - title: Account Detail + options: + fields: + - name: AccountName + - name: Type + - name: Industry + - name: NumberOfEmployees + - name: AnnualRevenue + - name: Twitter + - name: LinkedIn + - name: Facebook + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 3, 12] + - title: Account Address + options: + fields: + - name: BillingStreet + - name: BillingCity + - name: BillingState + - name: BillingPostalCode + - name: BillingCountry + - name: Phone + - name: Website + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [3, 0, 3, 12] + - title: Other Information + options: + fields: + - name: Rating + - name: ParentId + - name: AccountSource + - name: OwnerId + - name: Description + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [6, 0, 3, 12] + - title: Company Twitter Feed + options: + fields: [] + profileSourceField: Twitter + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: SocialFeed + xywh: [9, 0, 3, 20] + - title: Opportunities + options: + fields: + - name: Name + - name: StageName + - name: Amount + - name: CloseDate + hideAddButton: true + module: Opportunity + page: RecordPageForModuleOpportunity + perPage: 5 + prefilter: AccountId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 12, 6, 8] + - title: Contacts + options: + fields: + - name: FirstName + - name: LastName + - name: Title + hidePaging: true + module: Contact + page: RecordPageForModuleContact + perPage: 5 + prefilter: AccountId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 20, 4, 8] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: AccountId = ${recordID} + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 28, 4, 8] + - title: Cases + options: + fields: + - name: CaseNumber + - name: Subject + - name: Status + hideAddButton: true + module: Case + page: RecordPageForModuleCase + perPage: 5 + prefilter: AccountId = ${recordID} + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [8, 20, 4, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: AccountId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [4, 28, 4, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: AccountId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [8, 28, 4, 8] + - title: Contracts + options: + fields: + - name: Name + - name: StartDate + - name: EndDate + - name: Status + hideAddButton: true + module: Contract + page: RecordPageForModuleContract + perPage: 5 + prefilter: AccountId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [4, 20, 4, 8] + - title: Account Actions + options: + buttons: + - label: Generate a new Opportunity for this Account + script: AccountCreateNewOpportunity + variant: primary + - label: Generate a new Contract for this Account + script: AccountCreateNewContract + variant: primary + - label: Generate a new Case for this Account + script: AccountCreateNewCase + variant: primary + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Automation + xywh: [6, 12, 3, 8] + pages: + - handle: RecordPageForModuleAccountContactRole + module: AccountContactRole + title: Record page for module "AccountContactRole" + blocks: + - title: Account Contact Role Details + options: + fields: + - name: AccountId + - name: ContactId + - name: Role + - name: IsPrimary + kind: Record + xywh: [0, 0, 5, 15] + visible: false + visible: true + - handle: MyAccounts + title: My Accounts + visible: true + visible: true + - handle: Leads + title: Leads + blocks: + - title: List of leads + options: + fields: + - name: FirstName + - name: LastName + - name: Email + - name: Rating + - name: Company + - name: Country + - name: Status + - name: OwnerId + module: Lead + page: RecordPageForModuleLead + perPage: 20 + prefilter: "" + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 0, 9, 21] + - title: Leads by type + options: + chart: LeadsByType + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [9, 0, 3, 7] + - title: Leads by country + options: + chart: LeadsByCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [9, 7, 3, 7] + - title: Leads by source + options: + chart: LeadsBySource + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Chart + xywh: [9, 14, 3, 7] + pages: + - handle: RecordPageForModuleLead + module: Lead + title: Record page for module "Lead" + blocks: + - title: Personal Data + options: + fields: + - name: FirstName + - name: LastName + - name: Salutation + - name: Title + - name: Phone + - name: MobilePhone + - name: Email + - name: Website + - name: Twitter + - name: LinkedIn + - name: Facebook + kind: Record + xywh: [0, 0, 3, 15] + - title: Twitter feed + options: + fields: [] + profileSourceField: Twitter + kind: SocialFeed + xywh: [9, 0, 3, 31] + - title: Company Data + options: + fields: + - name: Company + - name: Industry + - name: AnnualRevenue + - name: NumberOfEmployees + - name: NumberOfEmployeesRange + - name: Country + - name: City + - name: Street + - name: State + - name: PostalCode + kind: Record + xywh: [3, 0, 3, 15] + - title: Other Information + options: + fields: + - name: Rating + - name: Status + - name: LeadSource + - name: OwnerId + - name: Description + - name: ConvertedAccountId + - name: ConvertedContactId + - name: ConvertedOpportunityId + - name: ConvertedDate + kind: Record + xywh: [6, 0, 3, 15] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: LeadId = ${recordID} + presort: createdAt DESC + kind: RecordList + xywh: [0, 15, 6, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: LeadId = ${recordID} + presort: ActivityDate DESC + kind: RecordList + xywh: [0, 23, 4, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: LeadId = ${recordID} + presort: ActivityDate DESC + kind: RecordList + xywh: [4, 23, 5, 8] + - title: Lead Actions + options: + buttons: + - label: Convert this Lead in to an Account + script: LeadConvertLeadIntoAccount + variant: primary + - label: 'Lead: Convert a lead into an account and opportunity' + script: LeadConvertLeadToAccountAndOpportunity + variant: primary + kind: Automation + xywh: [6, 15, 3, 8] + visible: true + - handle: AllNewLeads + title: All New Leads + blocks: + - title: New Leads + options: + fields: + - name: FirstName + - name: LastName + - name: Email + - name: Rating + - name: Company + - name: Country + - name: LeadSource + - name: Status + - name: OwnerId + module: Lead + page: RecordPageForModuleLead + perPage: 20 + prefilter: Status = 'New' OR Status = 'Assigned' + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + visible: true + - handle: MyLeads + title: My Leads + blocks: + - title: My Leads + options: + fields: + - name: FirstName + - name: LastName + - name: Email + - name: Rating + - name: Company + - name: Country + - name: LeadSource + - name: Status + - name: OwnerId + module: Lead + page: RecordPageForModuleLead + perPage: 20 + prefilter: OwnerId = ${userID} + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 21] + visible: true + visible: true + - handle: Opportunities + title: Opportunities + blocks: + - title: List of Opportunities + options: + fields: + - name: Name + - name: Amount + - name: AccountId + - name: StageName + - name: CloseDate + - name: OwnerId + - name: createdAt + module: Opportunity + page: RecordPageForModuleOpportunity + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleOpportunity + module: Opportunity + title: Record page for module "Opportunity" + blocks: + - title: Opportunity Detail + options: + fields: + - name: Name + - name: AccountId + - name: Type + - name: CloseDate + - name: Probability + - name: PricebookId + - name: Amount + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 4, 11] + - title: Additional Information + options: + fields: + - name: OwnerId + - name: NextStep + - name: LeadSource + - name: CampaignId + - name: ForecastCategory + - name: Description + - name: createdAt + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [8, 0, 4, 11] + - title: Contact Roles + options: + fields: + - name: ContactId + - name: IsPrimary + - name: Role + module: OpportunityContactRole + page: RecordPageForModuleOpportunityContactRole + perPage: 5 + prefilter: OpportunityId = ${recordID} + presort: IsPrimary DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [6, 19, 6, 8] + - title: Opportunity Qualification + options: + fields: + - name: StageName + - name: Budget + - name: Authority + - name: Need + - name: Timeline + - name: Competition + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [4, 0, 4, 11] + - title: Products + options: + fields: + - name: ProductId + - name: ProductCode + - name: ListPrice + - name: UnitPrice + - name: Quantity + - name: Subtotal + - name: Discount + - name: TotalPrice + module: OpportunityLineItem + page: RecordPageForModuleOpportunityLineItem + perPage: 5 + prefilter: OpportunityId = ${recordID} + presort: ProductId ASC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 11, 8, 8] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: OpportunityId = ${recordID} + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 27, 4, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: OpportunityId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [4, 27, 4, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: OpportunityId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [8, 27, 4, 8] + - title: Quotes + options: + fields: + - name: QuoteNumber + - name: Name + - name: ExpirationDate + - name: Subtotal + - name: TotalPrice + hideAddButton: true + module: Quote + page: RecordPageForModuleQuote + perPage: 5 + prefilter: OpportunityId = ${recordID} + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 19, 6, 8] + - title: Opportunity Actions + options: + buttons: + - label: Update prices of Products by applying the selected Price Book + and inserted discounts + script: OpportunityApplyPriceBook + variant: primary + - label: Generate a new Quote for this Opportunity + script: OpportunityGenerateNewQuote + variant: primary + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Automation + xywh: [8, 11, 4, 8] + pages: + - handle: RecordPageForModuleOpportunityLineItem + module: OpportunityLineItem + title: Record page for module "OpportunityLineItem" + blocks: + - title: Opportunity Line Item Details + options: + fields: + - name: OpportunityId + - name: ProductId + - name: Description + - name: UnitPrice + - name: Quantity + - name: Discount + kind: Record + xywh: [0, 0, 4, 13] + - title: Automatically inserted values + options: + fields: + - name: ProductCode + - name: ListPrice + - name: Subtotal + - name: TotalPrice + kind: Record + xywh: [4, 0, 4, 13] + visible: false + - handle: RecordPageForModuleOpportunityCompetitor + module: OpportunityCompetitor + title: Record page for module "OpportunityCompetitor" + blocks: + - title: Opportunity Competitor Details + options: + fields: + - name: OpportunityId + - name: CompetitorName + - name: Strengths + - name: Weaknesses + kind: Record + xywh: [0, 0, 5, 17] + visible: false + - handle: RecordPageForModuleOpportunityContactRole + module: OpportunityContactRole + title: Record page for module "OpportunityContactRole" + blocks: + - title: Opportunity Contact Role Details + options: + fields: + - name: OpportunityId + - name: ContactId + - name: Role + - name: IsPrimary + kind: Record + xywh: [0, 0, 4, 16] + pages: + - handle: RecordPageForModulePricebookEntry + module: PricebookEntry + title: Record page for module "PricebookEntry" + blocks: + - title: Details + options: + fields: + - name: PricebookId + - name: ProductId + - name: UnitPrice + kind: Record + xywh: [0, 0, 4, 15] + visible: false + visible: false + visible: false + - handle: MyOpportunities + title: My Opportunities + blocks: + - title: My Opportunities + options: + fields: + - name: Name + - name: Amount + - name: AccountId + - name: StageName + - name: CloseDate + - name: OwnerId + - name: createdAt + module: Opportunity + page: RecordPageForModuleOpportunity + perPage: 20 + prefilter: OwnerId = ${userID} + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + visible: true + visible: true + - handle: Contacts + title: Contacts + blocks: + - title: List of Contacts + options: + fields: + - name: FirstName + - name: LastName + - name: AccountId + - name: Phone + - name: Email + - name: OwnerId + module: Contact + page: RecordPageForModuleContact + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleContact + module: Contact + title: Record page for module "Contact" + blocks: + - title: Personal Data + options: + fields: + - name: FirstName + - name: LastName + - name: Salutation + - name: Title + - name: Phone + - name: Email + - name: Twitter + - name: Facebook + - name: LinkedIn + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 3, 13] + - title: Contact Address + options: + fields: + - name: AccountId + - name: IsPrimary + - name: MailingStreet + - name: MailingCity + - name: MailingState + - name: MailingPostalCode + - name: MailingCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [3, 0, 3, 13] + - title: Other Information + options: + fields: + - name: OwnerId + - name: DoNotCall + - name: HasOptedOutOfEmail + - name: ContactSource + - name: Description + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [6, 0, 3, 13] + - title: Contact Twitter Feed + options: + fields: [] + profileSourceField: Twitter + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: SocialFeed + xywh: [9, 0, 3, 20] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: LastModifiedDate + - name: CreatedById + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: ContactId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 21, 4, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: ContactId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [4, 21, 4, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: ContactId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [8, 21, 4, 8] + - title: Cases related with this Contact + options: + fields: + - name: CaseNumber + - name: Status + - name: Subject + - name: createdAt + hideAddButton: true + module: Case + page: RecordPageForModuleCase + perPage: 5 + prefilter: ContactId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 13, 6, 8] + - title: Contact Actions + options: + buttons: + - label: Create a new Case for this Contact + script: ContactCreateNewCase + variant: primary + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Automation + xywh: [6, 13, 3, 8] + visible: false + visible: true + - handle: Quotes + title: Quotes + blocks: + - title: List of Quotes + options: + fields: + - name: QuoteNumber + - name: Name + - name: OpportunityId + - name: Discount + - name: TotalPrice + - name: Tax + - name: GrandTotal + - name: createdAt + - name: ExpirationDate + module: Quote + page: RecordPageForModuleQuote + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleQuote + module: Quote + title: Record page for module "Quote" + blocks: + - title: Quote Information + options: + fields: + - name: QuoteNumber + - name: Name + - name: OpportunityId + - name: ExpirationDate + - name: Status + - name: Description + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 4, 12] + - title: Totals + options: + fields: + - name: PricebookId + - name: Subtotal + - name: Discount + - name: ShippingHandling + - name: TotalPrice + - name: Tax + - name: GrandTotal + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [8, 0, 4, 12] + - title: Primary contact data + options: + fields: + - name: AccountId + - name: ContactId + - name: Email + - name: Phone + - name: createdBy + - name: createdAt + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [4, 0, 4, 12] + - title: Bill To + options: + fields: + - name: BillingName + - name: BillingStreet + - name: BillingCity + - name: BillingState + - name: BillingPostalCode + - name: BillingCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 20, 4, 9] + - title: Quote To + options: + fields: + - name: QuoteToName + - name: QuoteToStreet + - name: QuoteToCity + - name: QuoteToState + - name: QuoteToPostalCode + - name: QuoteToCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [4, 20, 4, 9] + - title: Ship To + options: + fields: + - name: ShippingName + - name: ShippingStreet + - name: ShippingCity + - name: ShippingState + - name: ShippingPostalCode + - name: ShippingCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [8, 20, 4, 9] + - title: Quote Line Items + options: + fields: + - name: ProductId + - name: UnitPrice + - name: Quantity + - name: Subtotal + - name: Discount + - name: TotalPrice + - name: ListPrice + module: QuoteLineItem + page: RecordPageForModuleQuoteLineItem + perPage: 5 + prefilter: QuoteId = ${recordID} + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 12, 8, 8] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: QuoteId = ${recordID} + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 29, 4, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: QuoteId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [4, 29, 4, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: QuoteId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [8, 29, 4, 8] + - title: Quote Actions + options: + buttons: + - label: Submit this Quote for approval + script: QuoteSubmitQuoteForApproval + variant: primary + - label: Approve this Quote + script: QuoteApproveQuote + variant: primary + - label: Send this Quote via email to the primary contact + script: QuoteSendQuoteToPrimaryContact + variant: primary + - label: Send this Quote to a custom email + script: QuoteSendQuoteToCustomEmail + variant: primary + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Automation + xywh: [8, 12, 4, 8] + pages: + - handle: RecordPageForModuleQuoteLineItem + module: QuoteLineItem + title: Record page for module "QuoteLineItem" + blocks: + - title: Quote Line Item Information + options: + fields: + - name: QuoteId + - name: LineItemNumber + - name: LineItemDescription + - name: ProductId + - name: ListPrice + - name: UnitPrice + - name: Quantity + - name: Subtotal + - name: Discount + - name: TotalPrice + kind: Record + xywh: [0, 0, 4, 19] + visible: false + visible: false + visible: true + - handle: Products + title: Products + blocks: + - title: List of Products + options: + fields: + - name: Name + - name: ProductCode + - name: Description + - name: Family + module: Product + page: RecordPageForModuleProduct + perPage: 20 + presort: Name ASC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleProduct + module: Product + title: Record page for module "Product" + blocks: + - title: Product Details + options: + fields: + - name: Name + - name: ProductCode + - name: Family + - name: Description + - name: IsActive + kind: Record + xywh: [0, 0, 6, 10] + - title: Appears in the following Price Books + options: + fields: + - name: PricebookId + - name: UnitPrice + hideAddButton: true + module: PricebookEntry + page: RecordPageForModulePricebookEntry + perPage: 5 + prefilter: ProductId = ${recordID} + kind: RecordList + xywh: [6, 0, 6, 10] + - title: Related Cases + options: + fields: + - name: CaseNumber + - name: Subject + - name: Status + hideAddButton: true + module: Case + page: RecordPageForModuleCase + perPage: 5 + prefilter: ProductId = ${recordID} + presort: CaseNumber DESC + kind: RecordList + xywh: [0, 10, 6, 8] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: ProductId = ${recordID} + presort: createdAt DESC + kind: RecordList + xywh: [6, 10, 6, 8] + visible: false + - handle: PriceBook + title: Price book + blocks: + - title: Price books + options: + fields: + - name: Name + - name: Description + - name: IsStandard + - name: IsActive + module: Pricebook + page: RecordPageForModulePricebook + perPage: 20 + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleCaseContactRole + module: CaseContactRole + title: Record page for module "CaseContactRole" + blocks: + - title: Case Contact Role Details + options: + fields: + - name: CasesId + - name: ContactId + - name: Role + - name: IsPrimary + kind: Record + xywh: [0, 0, 5, 16] + visible: false + - handle: RecordPageForModulePricebook + module: Pricebook + title: Record page for module "Pricebook" + blocks: + - title: Price Book Details + options: + fields: + - name: Name + - name: Description + - name: IsStandard + - name: IsActive + kind: Record + xywh: [0, 0, 6, 14] + - title: Price Book Entries + options: + fields: + - name: ProductId + - name: UnitPrice + module: PricebookEntry + page: RecordPageForModulePricebookEntry + perPage: 12 + prefilter: PricebookId = ${recordID} + kind: RecordList + xywh: [6, 0, 6, 14] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: PricebookId = ${recordID} + presort: createdAt DESC + kind: RecordList + xywh: [0, 14, 12, 8] + visible: false + visible: true + visible: true + - handle: Contracts + title: Contracts + blocks: + - title: List of Contracts + options: + fields: + - name: ContractNumber + - name: AccountId + - name: Status + - name: StartDate + - name: EndDate + - name: ContractTerm + module: Contract + page: RecordPageForModuleContract + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleContract + module: Contract + title: Record page for module "Contract" + blocks: + - title: Contract Details + options: + fields: + - name: AccountId + - name: ContractNumber + - name: Status + - name: StartDate + - name: EndDate + - name: ContractTerm + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 4, 12] + - title: Contract Billing Address + options: + fields: + - name: BillingStreet + - name: BillingCity + - name: BillingState + - name: BillingPostalCode + - name: BillingCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 12, 4, 9] + - title: Contract Description and Special Terms + options: + fields: + - name: OwnerId + - name: Description + - name: SpecialTerms + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [4, 0, 4, 12] + - title: Contract Files + options: + fields: + - name: File1 + - name: File2 + - name: File3 + - name: File4 + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [8, 0, 4, 12] + - title: Contract Shipping Address + options: + fields: + - name: ShippingStreet + - name: ShippingCity + - name: ShippingState + - name: ShippingPostalCode + - name: ShippingCountry + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [4, 12, 4, 9] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: ContractId = ${recordID} + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 21, 4, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: ContractId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [4, 21, 4, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: ContractId = ${recordID} + presort: ActivityDate DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [8, 21, 4, 8] + pages: + - handle: RecordPageForModuleContractLineItem + module: ContractLineItem + title: Record page for module "ContractLineItem" + blocks: + - title: Contract Line Item Details + options: + fields: + - name: LineItemNumber + - name: Description + - name: StartDate + - name: EndDate + - name: Status + kind: Record + xywh: [0, 0, 4, 11] + - title: Contract Line Item Pricing + options: + fields: + - name: UnitPrice + - name: Quantity + - name: Subtotal + - name: Discount + - name: TotalPrice + kind: Record + xywh: [4, 0, 4, 11] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: ContractLineItemId = ${recordID} + kind: RecordList + xywh: [8, 0, 4, 11] + visible: false + - handle: RecordPageForModuleEntitlement + module: Entitlement + title: Record page for module "Entitlement" + blocks: + - title: Entitlement Details + options: + fields: + - name: Name + - name: Entitlement Process + - name: CasesPerEntitlement + - name: Type + - name: RemainingCases + - name: IsPerIncident + - name: StartDate + - name: EndDate + - name: Status + kind: Record + xywh: [0, 0, 4, 13] + - title: Other Information + options: + fields: + - name: AccountId + - name: ContractLineItemId + - name: CreatedById + - name: CreatedDate + - name: LastModifiedById + - name: LastModifiedDate + kind: Record + xywh: [4, 0, 4, 13] + - title: Notes + options: + fields: + - name: Title + - name: Body + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: EntitlementId = ${recordID} + kind: RecordList + xywh: [6, 13, 6, 7] + - title: Entitlement Templates + options: + fields: + - name: Name + - name: Type + - name: CasesPerEntitlement + - name: IsPerIncident + - name: Term + module: EntitlementTemplate + page: RecordPageForModuleEntitlementTemplate + perPage: 5 + prefilter: EntitlementId = ${recordID} + kind: RecordList + xywh: [0, 13, 6, 7] + - title: Entitlement Contacts + options: + fields: + - name: ContactId + - name: Name + module: EntitlementContact + page: RecordPageForModuleEntitlementContact + perPage: 8 + prefilter: EntitlementId = ${recordID} + kind: RecordList + xywh: [8, 0, 4, 13] + pages: + - handle: RecordPageForModuleEntitlementTemplate + module: EntitlementTemplate + title: Record page for module "EntitlementTemplate" + blocks: + - title: Entitlement Template Details + options: + fields: + - name: Name + - name: Type + - name: CasesPerEntitlement + - name: IsPerIncident + - name: Term + - name: BusinessHours + kind: Record + xywh: [0, 0, 4, 14] + visible: false + - handle: RecordPageForModuleEntitlementContact + module: EntitlementContact + title: Record page for module "EntitlementContact" + blocks: + - title: Entitlement Contact Details + options: + fields: + - name: EntitlementId + - name: ContactId + - name: Name + kind: Record + xywh: [0, 0, 4, 14] + visible: false + visible: false + - handle: RecordPageForModuleContractContactRole + module: ContractContactRole + title: Record page for module "ContractContactRole" + blocks: + - title: Contract Contact Role Details + options: + fields: + - name: ContractId + - name: ContactId + - name: Role + - name: IsPrimary + kind: Record + xywh: [0, 0, 4, 16] + visible: false + visible: false + visible: true + - handle: Cases + title: Cases + blocks: + - title: List of Cases + options: + fields: + - name: CaseNumber + - name: Subject + - name: Status + - name: CreatedDate + - name: OwnerId + - name: createdAt + module: Case + page: RecordPageForModuleCase + perPage: 20 + presort: createdAt DESC + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleCase + module: Case + title: Record page for module "Case" + blocks: + - title: Case Main Information + options: + fields: + - name: CaseNumber + - name: Subject + - name: AccountId + - name: ContactId + - name: SuppliedName + - name: SuppliedEmail + - name: SuppliedPhone + kind: Record + xywh: [0, 0, 3, 14] + - title: Case Details + options: + fields: + - name: Type + - name: Description + - name: File + - name: Origin + - name: ProductId + kind: Record + xywh: [3, 0, 3, 14] + - title: Case Status + options: + fields: + - name: Status + - name: OwnerId + - name: CreatedDate + - name: Priority + - name: IsEscalated + - name: IsClosed + - name: ClosedDate + - name: Reason + kind: Record + xywh: [9, 0, 3, 14] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: CaseId = ${recordID} + presort: createdAt DESC + kind: RecordList + xywh: [0, 22, 4, 8] + - title: Events + options: + fields: + - name: Type + - name: Subject + - name: ActivityDate + module: Event + page: RecordPageForModuleEvent + perPage: 5 + prefilter: CaseId = ${recordID} + presort: ActivityDate DESC + kind: RecordList + xywh: [4, 22, 5, 8] + - title: Tasks + options: + fields: + - name: Type + - name: Subject + - name: Status + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 5 + prefilter: CaseId = ${recordID} + presort: ActivityDate DESC + kind: RecordList + xywh: [9, 22, 3, 8] + - title: Case Updates + options: + fields: + - name: Type + - name: Description + - name: File + - name: createdBy + - name: createdAt + module: CaseUpdate + page: RecordPageForModuleCaseUpdate + perPage: 5 + prefilter: CaseId = ${recordID} + presort: createdAt DESC + kind: RecordList + xywh: [0, 14, 9, 8] + - title: Case Actions + options: + buttons: + - label: Set Case status to Working + script: CaseSetStatusToWorking + variant: primary + - label: Set Case status to Closed + script: CaseSetStatusToClosed + variant: primary + - label: Inform the contact via email of the Case's solution + script: CaseInformContactOfSolution + variant: primary + kind: Automation + xywh: [9, 14, 3, 8] + - title: Case Solution + options: + fields: + - name: SolutionName + - name: SolutionNote + - name: SolutionFile + - name: SolutionId + - name: SubmitAsSolution + kind: Record + xywh: [6, 0, 3, 14] + pages: + - handle: RecordPageForModuleCaseUpdate + module: CaseUpdate + title: Record page for module "CaseUpdate" + blocks: + - title: Case Update + options: + fields: + - name: CaseId + - name: Description + - name: Type + - name: File + - name: IsSolution + - name: createdBy + - name: createdAt + kind: Record + xywh: [0, 0, 4, 20] + visible: false + - handle: RecordPageForModuleCaseMilestone + module: CaseMilestone + title: Record page for module "CaseMilestone" + blocks: + - title: Case Milestone Details + options: + fields: + - name: CaseId + - name: TargetDate + - name: TargetResponseInDays + - name: TargetResponseInHrs + - name: TargetResponseInMins + - name: StartDate + - name: CompletionDate + - name: IsViolated + - name: IsCompleted + kind: Record + xywh: [0, 0, 5, 19] + pages: + - handle: RecordPageForModuleMilestoneType + module: MilestoneType + title: Record page for module "MilestoneType" + blocks: + - title: Case Milestone Type Details + options: + fields: + - name: Name + - name: Description + - name: RecurrenceType + kind: Record + xywh: [0, 0, 4, 15] + visible: false + visible: false + visible: false + - handle: Solutions + title: Solutions + blocks: + - title: List of Solutions + options: + fields: + - name: SolutionNumber + - name: SolutionName + - name: SolutionNote + - name: CaseId + - name: ProductId + module: Solution + page: RecordPageForModuleSolution + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleSolution + module: Solution + title: Record page for module "Solution" + blocks: + - title: Solution Details + options: + fields: + - name: SolutionNumber + - name: SolutionName + - name: SolutionNote + - name: File + - name: CaseId + - name: ProductId + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 5, 17] + visible: false + visible: true + visible: true + - handle: Campaigns + title: Campaigns + blocks: + - title: List of Campaigns + options: + fields: + - name: Name + - name: ParentId + - name: Type + - name: Status + - name: StartDate + - name: EndDate + module: Campaigns + page: RecordPageForModuleCampaign + perPage: 20 + prefilter: "" + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleCampaign + module: Campaigns + title: Record page for module "Campaign" + blocks: + - title: Campaign Details + options: + fields: + - name: Name + - name: IsActive + - name: ParentId + - name: Type + - name: OwnerId + - name: Status + - name: StartDate + - name: EndDate + kind: Record + xywh: [0, 0, 4, 13] + - title: Campaign Planning + options: + fields: + - name: BudgetedCost + - name: ActualCost + - name: ExpectedRevenue + - name: CreatedById + - name: CreatedDate + - name: Description + kind: Record + xywh: [4, 0, 4, 13] + - title: Campaign Statistics + options: + fields: + - name: NumberOfLeads + - name: NumberOfConvertedLeads + - name: NumberOfContacts + - name: NumberOfOpportunities + - name: NumberOfWonOpportunities + - name: AmountAllOpportunities + - name: AmountWonOpportunities + kind: Record + xywh: [8, 0, 4, 13] + - title: Campaign Members + options: + fields: + - name: LeadId + - name: ContactId + - name: Status + module: CampaignMember + page: RecordPageForModuleCampaignMember + perPage: 5 + prefilter: CampaignId = ${recordID} + kind: RecordList + xywh: [0, 13, 8, 8] + - title: Generated Opportunities + options: + fields: + - name: Name + - name: StageName + - name: Amount + - name: CloseDate + module: Opportunity + page: RecordPageForModuleOpportunity + perPage: 5 + prefilter: CampaignId = ${recordID} + kind: RecordList + xywh: [0, 21, 8, 8] + - title: Notes + options: + fields: + - name: Title + - name: Body + - name: createdAt + module: Note + page: RecordPageForModuleNote + perPage: 5 + prefilter: CampaignId = ${recordID} + presort: createdAt DESC + kind: RecordList + xywh: [8, 21, 4, 8] + pages: + - handle: RecordPageForModuleCampaignMember + module: CampaignMember + title: Record page for module "CampaignMember" + blocks: + - title: Campaign Member Details + options: + fields: + - name: CampaignId + - name: LeadId + - name: ContactId + - name: HasResponded + - name: FirstRespondedDate + - name: Status + kind: Record + xywh: [0, 0, 5, 13] + pages: + - handle: RecordPageForModuleCampaignMemberStatus + module: 'Error: module with ID 69055788664029201 does not exist' + title: Record page for module "CampaignMemberStatus" + visible: false + visible: false + visible: false + visible: true + - handle: Tasks + title: Tasks + blocks: + - title: List of Tasks + options: + fields: + - name: Subject + - name: Status + - name: LeadId + - name: AccountId + - name: ContactId + - name: OpportunityId + - name: QuoteId + - name: ContractId + - name: CaseId + - name: ActivityDate + module: Task + page: RecordPageForModuleTask + perPage: 20 + prefilter: "" + presort: ActivityDate DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleTask + module: Task + title: Record page for module "Task" + blocks: + - title: Task Details + options: + fields: + - name: Subject + - name: Description + - name: Type + kind: Record + xywh: [0, 0, 4, 20] + - title: Related To + options: + fields: + - name: LeadId + - name: AccountId + - name: ContactId + - name: OpportunityId + - name: QuoteId + - name: ContractId + kind: Record + xywh: [8, 0, 4, 20] + - title: Task Status + options: + fields: + - name: Status + - name: ActivityDate + - name: Priority + - name: createdBy + - name: createdAt + kind: Record + xywh: [4, 0, 4, 20] + visible: false + visible: true + - handle: Events + title: Events + blocks: + - title: List of Events + options: + fields: + - name: WhoId + - name: Subject + - name: ActivityDate + - name: StartDateTime + - name: EndDateTime + - name: LeadId + - name: AccountId + - name: OpportunityId + - name: createdBy + module: Event + page: RecordPageForModuleEvent + perPage: 20 + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleEvent + module: Event + title: Record page for module "Event" + blocks: + - title: Event Data + options: + fields: + - name: Subject + - name: Description + - name: Location + - name: createdAt + - name: createdBy + kind: Record + xywh: [0, 0, 4, 20] + - title: Related Objects + options: + fields: + - name: LeadId + - name: AccountId + - name: ContactId + - name: OpportunityId + - name: QuoteId + - name: ContractId + - name: CaseId + kind: Record + xywh: [8, 0, 4, 20] + - title: Event Date and Time + options: + fields: + - name: ActivityDate + - name: DurationInMinutes + - name: EndDateTime + - name: IsAllDayEvent + kind: Record + xywh: [4, 0, 4, 20] + visible: false + visible: true + - handle: Notes + title: Notes + blocks: + - title: List of Notes + options: + fields: + - name: Title + - name: LeadId + - name: AccountId + - name: ContactId + - name: OpportunityId + - name: QuoteId + - name: ContractId + - name: CaseId + - name: createdAt + - name: createdBy + module: Note + page: RecordPageForModuleNote + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleNote + module: Note + title: Record page for module "Note" + blocks: + - title: Note + options: + fields: + - name: Title + - name: Body + - name: createdAt + - name: createdBy + kind: Record + xywh: [0, 0, 4, 20] + - title: Files + options: + fields: + - name: FileAdd1 + - name: FileAdd2 + - name: FileAdd3 + - name: FileAdd4 + kind: Record + xywh: [4, 0, 4, 20] + - title: Related Objects + options: + fields: + - name: LeadId + - name: AccountId + - name: ContactId + - name: OpportunityId + - name: QuoteId + - name: ContractId + - name: CaseId + kind: Record + xywh: [8, 0, 4, 20] + visible: false + visible: true + - handle: Dashboards + title: Dashboards + blocks: + - title: Content + options: + body:

Each page in Corteza can be a dashboard. You can add text blocks, + record lists or charts to make a Sales Activities Dashboard, an executive + dashboard, or any other dashboard you may need, and set the access + permissions to define who can view or edit the dashboard.

+ kind: Content + xywh: [0, 0, 5, 16] + - title: Leads per day + options: + chart: LeadsPerDay + kind: Chart + xywh: [5, 0, 3, 16] + pages: + - handle: DefaultSettings + title: Default Settings + description: A list of defaults values which are called upon in automation + processes. + blocks: + - title: Default settings + options: + fields: + - name: createdAt + - name: updatedAt + hidePaging: true + module: DefaultSettings + page: RecordPageForModuleDefaultSettings + perPage: 1 + kind: RecordList + xywh: [0, 0, 12, 7] + pages: + - handle: RecordPageForModuleDefaultSettings + module: DefaultSettings + title: Record page for module "DefaultSettings" + blocks: + - title: Quote Settings + options: + fields: + - name: QuoteNextNumber + - name: QuoteExpirationDays + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [3, 0, 3, 12] + - title: Opportunity Settings + options: + fields: + - name: OpportunityCloseDateDays + - name: OpportunityProbability + - name: OpportunityForecaseCategory + - name: OpportunityStagename + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [0, 0, 3, 12] + - title: Contract Settings + options: + fields: + - name: ContractNextNumber + - name: ContractDefaultTime + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [6, 0, 3, 12] + - title: Case Settings + options: + fields: + - name: CaseNextNumber + - name: SolutionNextNumber + style: + variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary} + kind: Record + xywh: [9, 0, 3, 12] + visible: false + visible: true + visible: true + - handle: UnpublishedModules + title: Unpublished modules + pages: + - handle: Questions + title: Questions + visible: false + blocks: + - title: List of Questions + options: + fields: + - name: Title + - name: CreatedDate + - name: NumReplies + module: Question + page: RecordPageForModuleQuestion + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleQuestion + module: Question + title: Record page for module "Question" + blocks: + - title: Question Details + options: + fields: + - name: Title + - name: Body + - name: NumReplies + - name: BestReplyId + - name: UpVotes + - name: VoteScore + kind: Record + xywh: [0, 0, 4, 13] + - title: Replies + options: + fields: + - name: Body + - name: Name + - name: UpVotes + - name: DownVotes + - name: VoteTotal + module: Reply + page: RecordPageForModuleReply + perPage: 5 + prefilter: QuestionId = ${recordID} + kind: RecordList + xywh: [0, 13, 12, 7] + - title: Votes + options: + fields: + - name: Score + module: Vote + page: RecordPageForModuleVote + perPage: 8 + prefilter: QuestionId = ${recordID} + kind: RecordList + xywh: [4, 0, 8, 13] + pages: + - handle: RecordPageForModuleVote + module: Vote + title: Record page for module "Vote" + blocks: + - title: Vote Score + options: + fields: + - name: Score + - name: CreatedById + - name: CreatedDate + kind: Record + xywh: [0, 0, 4, 13] + - title: Related Object + options: + fields: + - name: IdeaId + - name: ReplyId + - name: QuestionId + kind: Record + xywh: [4, 0, 4, 13] + visible: false + - handle: RecordPageForModuleReply + module: Reply + title: Record page for module "Reply" + blocks: + - title: Reply Details + options: + fields: + - name: QuestionId + - name: IdeaId + - name: Body + - name: Name + - name: UpVotes + - name: DownVotes + - name: VoteTotal + kind: Record + xywh: [0, 0, 6, 20] + - title: Votes + options: + fields: + - name: Score + - name: CreatedById + - name: CreatedDate + module: Vote + page: RecordPageForModuleVote + perPage: 20 + prefilter: ReplyId = ${recordID} + kind: RecordList + xywh: [6, 0, 6, 20] + visible: false + visible: false + - handle: Ideas + title: Ideas + blocks: + - title: List of Ideas + options: + fields: + - name: Title + - name: Categories + - name: ParentIdeaId + - name: VoteScore + - name: Status + module: Idea + page: RecordPageForModuleIdea + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleIdea + module: Idea + title: Record page for module "Idea" + blocks: + - title: Idea Details + options: + fields: + - name: Title + - name: Body + - name: Categories + - name: ParentIdeaId + - name: CreatorName + - name: Status + - name: VoteScore + - name: VoteTotal + kind: Record + xywh: [0, 0, 4, 20] + - title: Votes + options: + fields: + - name: Score + - name: CreatedById + - name: CreatedDate + module: Vote + page: RecordPageForModuleVote + perPage: 20 + prefilter: IdeaId = ${recordID} + kind: RecordList + xywh: [8, 0, 4, 20] + - title: Replies + options: + fields: + - name: Body + - name: Name + - name: UpVotes + - name: DownVotes + - name: VoteTotal + module: Reply + page: RecordPageForModuleReply + perPage: 20 + prefilter: IdeaId = ${recordID} + kind: RecordList + xywh: [4, 0, 4, 20] + visible: false + visible: false + - handle: Emails + title: Emails + blocks: + - title: List of Email Templates + options: + fields: + - name: Name + - name: IsActive + - name: createdBy + module: EmailTemplate + page: RecordPageForModuleEmailTemplate + perPage: 20 + presort: createdAt DESC + kind: RecordList + xywh: [0, 0, 12, 20] + pages: + - handle: RecordPageForModuleEmailTemplate + module: EmailTemplate + title: Record page for module "EmailTemplate" + blocks: + - title: Email Template Details + options: + fields: + - name: Name + - name: Subject + - name: Description + - name: Body + - name: IsActive + kind: Record + xywh: [0, 0, 6, 19] + visible: false + visible: false + visible: false diff --git a/provision/compose/static.go b/provision/compose/static.go new file mode 100644 index 000000000..507a03997 --- /dev/null +++ b/provision/compose/static.go @@ -0,0 +1,6 @@ +// Code generated by statik. DO NOT EDIT. + +// Package contains static assets. +package compose + +var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00 \x000000_access_control.yamlUT\x05\x00\x01\x80Cm8allow:\n everyone:\n compose:\n - access\n\n admins:\n compose:\n - access\n - grant\n - namespace.create\n\n compose:namespace:\n - read\n - update\n - delete\n - manage\n - page.create\n - module.create\n - chart.create\n\n compose:module:\n - read\n - update\n - delete\n - record.create\n - record.read\n - record.update\n - record.delete\n\n compose:chart:\n - read\n - update\n - delete\n\n compose:page:\n - read\n - update\n - delete\n\n compose:automation-script:\n - read\n - update\n - delete\nPK\x07\x08\xf6e\x19 0\n metrics:\n - aggregate: SUM\n axisPosition: left\n axisType: linear\n backgroundColor: '#009eff'\n beginAtZero: true\n field: Amount\n fill: true\n label: Amount\n type: line\n dimensions:\n - autoSkip: true\n conditions: {}\n field: CloseDate\n modifier: QUARTER\n module: Opportunity\nPK\x07\x08\x18\x18\xf1\xc2\xb0 \x00\x00\xb0 \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x001300_scripts.yamlUT\x05\x00\x01\x80Cm8namespace: crm\nscripts:\n AccountCreateNewOpportunity:\n source: |-\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n\n let opportunityCloseDays = defaultSettings.values.OpportunityCloseDateDays\n let opportunityProbability = defaultSettings.values.OpportunityProbability\n let opportunityForecaseCategory = defaultSettings.values.OpportunityForecaseCategory\n let opportunityStagename = defaultSettings.values.OpportunityStagename\n\n //Calculate the expiration date\n let m = new Date()\n m.setDate(m.getDate() + parseInt(opportunityCloseDays, 10))\n let closeDate = m.getUTCFullYear() + \"/\" + (m.getUTCMonth() + 1) + \"/\" + m.getUTCDate() + \" \" + m.getUTCHours() + \":\" + m.getUTCMinutes() + \":\" + m.getUTCSeconds()\n\n // Find the contact we want to link the new case to (by default, the primary contact)\n return Compose.findRecords(`AccountId = ${$record.recordID}`, 'Contact')\n\n .then(({ set, filter }) => {\n\n let ContactId, SuppliedName, SuppliedEmail, SuppliedPhone\n\n // Loop through the contacts of the account, to save the primary contact\n set.forEach(r => {\n\n //Check if it's the primary contact\n let contactIsPrimary = r.values.IsPrimary\n if (contactIsPrimary === '1') {\n\n //Add the contact\n ContactId = r.recordID\n }\n })\n\n // Create the related opportunity\n return Compose.makeRecord({\n 'OwnerId': $record.values.OwnerId,\n 'LeadSource': $record.values.LeadSource,\n 'Name': '(unnamed)',\n 'AccountId': $record.recordID,\n 'IsClosed': 'No',\n 'IsWon': 'No',\n 'CloseDate': closeDate,\n 'Probability': opportunityProbability,\n 'ForecastCategory': opportunityForecaseCategory,\n 'StageName': opportunityStagename\n }, 'Opportunity')\n\n .then(myOpportunity => {\n\n return Compose.saveRecord(myOpportunity)\n\n }).then(mySavedOpportunity => {\n\n //Create a new contact linked to the opportunity\n return Compose.makeRecord({\n 'ContactId': ContactId,\n 'OpportunityId': mySavedOpportunity.recordID,\n 'IsPrimary': '1'\n }, 'OpportunityContactRole')\n\n .then(myOpportunityContactRole => {\n\n return Compose.saveRecord(myOpportunityContactRole)\n\n .then(() => {\n\n // Notify current user\n ComposeUI.success(`The new opportunity has been created.`)\n\n\n }).then(() => {\n\n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedOpportunity)\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n CaseInsertCaseNumber:\n source: |-\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n \n // Map the case number\n let nextCaseNumber = defaultSettings.values.CaseNextNumber\n if (typeof nextCaseNumber == \"undefined\" || nextCaseNumber === '' || isNaN(nextCaseNumber)) {\n nextCaseNumber = 0\n }\n \n $record.values.CaseNumber = nextCaseNumber\n let nextCaseNumberUpdated = parseInt(nextCaseNumber,10) + 1\n \n // Update the config\n defaultSettings.values.CaseNextNumber = nextCaseNumberUpdated\n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n \n console.log('Record saved, new ID', mySavedDefaultSettings.recordID)\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - enabled: false\n event: afterCreate\n module: Case\n resource: compose:record\n - event: beforeCreate\n module: Case\n resource: compose:record\n CaseInformContactOfSolution:\n source: |-\n // Check if the case is solved\n if (!$record.values.Status === 'Closed') {\n\n // Get the to address\n let to = $record.values.SuppliedEmail\n if (!to) {\n ComposeUI.warning(`There is no supplied email. Please fill in an email address in the supplied email field.`)\n return\n }\n\n // Get email body\n let html = \"

Solution of case: \" + $record.values.CaseNumber + \" - \" + $record.values.Subject + \"

\"\n html = html + \"
\"\n html = html + \"Solution Title: \" + $record.values.SolutionName + \"
\"\n html = html + \"Solution Details: \" + $record.values.SolutionNote\n\n // Send the email\n return Compose.sendMail(to, `Corteza - Quote: ${$record.values.QuoteNumber} - ${$record.values.Name}`, { html: html }).then(() => {\n ComposeUI.success(`The case solutoin has been sent via email.`)\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n } else {\n ComposeUI.warning(`You can only inform the client of a solution when the case status is \"Closed\".`)\n return\n }\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n ContactSetRecordLabel:\n source: |-\n // Set the record label string\n let recordLabel = ''\n\n // Get the first name\n let firstName = $record.values.FirstName\n if (!firstName) {\n firstName = ''\n }\n\n // Get the last name\n let lastName = $record.values.LastName\n if (!lastName) {\n lastName = ''\n }\n\n // Create the full name\n let fullName = ''\n if ((firstName !== '') && (lastName === '')) {\n recordLabel = firstName\n }\n\n if ((firstName === '') && (lastName !== '')) {\n recordLabel = lastName\n }\n\n if ((firstName !== '') && (lastName !== '')) {\n recordLabel = firstName + ' ' + lastName\n }\n\n // Get the company name from the account\n // Check if there is a related account, to map the fields of the account\n let accountId = $record.values.AccountId\n if (accountId) {\n return Compose.findRecordByID(accountId, 'Account').then(accountRecord => {\n \n // Add to the record label \n recordLabel = recordLabel + ' (' + accountRecord.values.AccountName + ')'\n $record.values.RecordLabel = recordLabel\n\n }).catch(err => {\n // solve the problem\n $record.values.RecordLabel = recordLabel\n\n console.error(err)\n })\n \n }\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - enabled: false\n event: afterCreate\n module: Contact\n resource: compose:record\n - enabled: false\n event: afterUpdate\n module: Contact\n resource: compose:record\n - event: beforeUpdate\n module: Contact\n resource: compose:record\n - event: beforeCreate\n module: Contact\n resource: compose:record\n LeadSetRecordLabel:\n source: |-\n // Set the record label string\n let recordLabel = ''\n\n // Get the first name\n let firstName = $record.values.FirstName\n if (!firstName) {\n fistName = ''\n }\n\n // Get the last name\n let lastName = $record.values.LastName\n if (!lastName) {\n lastName = ''\n }\n\n // Create the full name\n let fullName = ''\n if ((firstName !== '') && (lastName === '')) {\n recordLabel = firstName\n }\n\n if ((firstName === '') && (lastName !== '')) {\n recordLabel = lastName\n }\n\n if ((firstName !== '') && (lastName !== '')) {\n recordLabel = firstName + ' ' + lastName\n }\n\n // Get the company name\n let company = $record.values.Company\n if (!company) {\n company = ''\n }\n\n // Add the company name (if there is one)\n if (company !== '') {\n recordLabel = recordLabel + ' (' + company + ')'\n }\n\n // Set the label\n $record.values.RecordLabel = recordLabel\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - enabled: false\n event: afterCreate\n module: Lead\n resource: compose:record\n - enabled: false\n event: afterUpdate\n module: Lead\n resource: compose:record\n - event: beforeUpdate\n module: Lead\n resource: compose:record\n - event: beforeCreate\n module: Lead\n resource: compose:record\n QuoteApproveQuote:\n source: |-\n //Check if the quote has the correct status\n if ($record.values.Status !== 'In Review') {\n // Inform\n ComposeUI.warning(`A quote needs to have the status In Review in order to be approved.`)\n return true\n }\n\n //Change value\n $record.values.Status = 'Approved'\n\n return Compose.saveRecord($record)\n .then(mySavedRecord => {\n\n // Get the email of the owner\n return System.findUserByID($record.createdBy).then(user => {\n\n // Send the mail\n return Compose.sendMail(\n user.email,\n `Quote \"${$record.values.Name}\" has been approved`,\n { html: `The following quote has been approved:

${$record.values.Name}` }\n )\n }).then(() => {\n\n // Notify current user\n ComposeUI.success(`The quote has been approved and the quote owner has been notified via email.`)\n\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n SolutionInsertSolutionNumber:\n source: |-\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n \n // Map the case number\n let nextSolutionNumber = defaultSettings.values.SolutionNextNumber\n if (typeof nextSolutionNumber == \"undefined\" || nextSolutionNumber === '' || isNaN(nextSolutionNumber)) {\n nextSolutionNumber = 0\n }\n $record.values.SolutionNumber = nextSolutionNumber\n let nextSolutionNumberUpdated = parseInt(nextSolutionNumber,10) + 1\n \n // Update the config\n defaultSettings.values.SolutionNextNumber = nextSolutionNumberUpdated\n \n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n console.log('Record saved, new ID', mySavedDefaultSettings.recordID)\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - enabled: false\n event: afterCreate\n module: Solution\n resource: compose:record\n - event: beforeCreate\n module: Solution\n resource: compose:record\n AccountCreateNewContract:\n source: |-\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n \n // Map the case number\n let ContractDefaultTime = defaultSettings.values.ContractDefaultTime\n\n // Get the contract number\n let nextContractNumber = defaultSettings.values.ContractNextNumber\n if (typeof nextContractNumber == \"undefined\" || nextContractNumber === '' || isNaN(nextContractNumber)) {\n nextContractNumber = 0\n }\n \n return Compose.makeRecord({\n 'OwnerId' : $record.values.OwnerId,\n 'AccountId' : $record.recordID,\n 'Status' : 'Draft',\n 'BillingStreet' : $record.values.BillingStreet,\n 'BillingCity' : $record.values.BillingCity,\n 'BillingState' : $record.values.BillingState,\n 'BillingPostalCode' : $record.values.BillingPostalCode,\n 'BillingCountry' : $record.values.BillingCountry,\n 'ShippingStreet' : $record.values.BillingStreet,\n 'ShippingCity' : $record.values.BillingCity,\n 'ShippingState' : $record.values.BillingState,\n 'ShippingPostalCode' : $record.values.BillingPostalCode,\n 'ShippingCountry' : $record.values.BillingCountry,\n 'ContractTerm' : ContractDefaultTime,\n 'ContractNumber' : nextContractNumber\n }, 'Contract').then(myContract => {\n \n // return was missing here. Set tjhe mySavedCase below\n return Compose.saveRecord(myContract)\n \n }).then(mySavedContract => {\n let nextContractNumberUpdated = parseInt(nextContractNumber,10) + 1\n \n // Update the config\n defaultSettings.values.ContractNextNumber = nextContractNumberUpdated\n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n \n console.log('Record saved, new ID', mySavedDefaultSettings.recordID)\n\n }).then(() => {\n\n // Notify current user\n ComposeUI.success(`The new contract record has been created.`)\n \n }).then(() => {\n\n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedContract)\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n CaseSetStatusToWorking:\n source: |-\n // Update the status\n $record.values.Status = 'Working'\n let caseRecord = $record\n\n // Save the case\n return Compose.makeRecord({\n 'CaseId' : caseRecord.recordID,\n 'Description' : 'State set to \"Working\"',\n 'Type' : 'State change'\n }, 'CaseUpdate').then(myCaseUpdate => {\n\n return Compose.saveRecord(myCaseUpdate)\n \n }).then(myCaseUpdate => {\n\n return Compose.saveRecord(caseRecord)\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n CaseSetStatusToClosed:\n source: |-\n // Check if the case is already closed\n if ($record.values.Status === 'Closed') {\n // Case is closed already. Exit\n ComposeUI.success(`This case is already closed.`)\n return true\n }\n\n // Check if there is a solution\n let SolutionName = $record.values.SolutionName\n let solutionRecord = $record.values.SolutionId\n\n // If there is no solution, show that it's not possible to close the case\n if ((!SolutionName) && (!solutionRecord)) {\n ComposeUI.warning(`Unable to close the case. Please add a solution name or select an existing solution before closing the case.`)\n return true\n }\n\n // Update the status\n $record.values.IsClosed = true\n var m = new Date()\n var dateString = m.getUTCFullYear() + \"/\" + (m.getUTCMonth() + 1) + \"/\" + m.getUTCDate() + \" \" + m.getUTCHours() + \":\" + m.getUTCMinutes() + \":\" + m.getUTCSeconds()\n $record.values.ClosedDate = dateString\n $record.values.Status = 'Closed'\n\n return Compose.saveRecord($record)\n .then(mySavedRecord => {\n\n // Create the CaseUpdate record\n return Compose.makeRecord({\n 'Description': 'State set to \"Closed',\n 'Type': 'State change',\n 'CaseId': $record.recordID\n }, 'CaseUpdate')\n\n .then(myCaseUpdate => {\n\n return Compose.saveRecord(myCaseUpdate)\n\n }).then(mySavedCaseUpdate => {\n\n // Check if a solution record has been selected\n if (solutionRecord) {\n\n // If there is a solution record, map the values in the case\n return Compose.findRecordByID(solutionRecord, 'Solution').then(solution => {\n $record.values.SolutionName = solution.values.SolutionName\n $record.values.SolutionNote = solution.values.SolutionNote\n $record.values.SolutionFile = solution.values.File\n\n return Compose.saveRecord($record)\n }).catch(err => {\n\n // solve the problem\n console.error(err)\n })\n } else {\n\n // If there is no solution record, check if the value \"SubmitAsSolution\" is checked. If so, save the solution as a Solution record\n if ($record.values.SubmitAsSolution) {\n\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n\n // Map the solution number\n let nextSolutionNumber = defaultSettings.values.SolutionNextNumber\n if (typeof nextSolutionNumber == \"undefined\" || nextSolutionNumber === '' || isNaN(nextSolutionNumber)) {\n nextSolutionNumber = 0\n }\n\n // Create the Solution record\n return Compose.makeRecord({\n 'SolutionName': $record.values.SolutionName,\n 'SolutionNote': $record.values.SolutionNote,\n 'File': $record.values.SolutionFile,\n 'Status': 'New',\n 'IsPublished': '1',\n 'CaseId': $record.recordID,\n 'SolutionNumber': nextSolutionNumber,\n 'ProductId': $record.values.ProductId\n }, 'Solution')\n\n .then(mySolution => {\n\n return Compose.saveRecord(mySolution)\n\n }).then(mySavedSolution => {\n\n // Save the solution record in the case record\n $record.values.SolutionId = mySavedSolution.recordID\n return Compose.saveRecord($record)\n\n }).then(mySavedSolution => {\n\n let nextSolutionNumberUpdated = parseInt(nextSolutionNumber, 10) + 1\n\n // Update the config\n defaultSettings.values.SolutionNextNumber = nextSolutionNumberUpdated\n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n\n console.log('Record saved, ID', mySavedDefaultSettings.recordID)\n\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }\n }\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n AccountCreateNewCase:\n source: |-\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n \n // Map the case number\n let nextCaseNumber = defaultSettings.values.CaseNextNumber\n if (typeof nextCaseNumber == \"undefined\" || nextCaseNumber === '' || isNaN(nextCaseNumber)) {\n nextCaseNumber = 0\n }\n \n // Find the contact we want to link the new case to (by default, the primary contact)\n return Compose.findRecords(`AccountId = ${$record.recordID}`, 'Contact').then(({ set, filter }) => {\n \n let ContactId, SuppliedName, SuppliedEmail, SuppliedPhone\n\n // Loop through the contacts of the account, to save the primary contact\n set.forEach(r => {\n \n //Check if it's the primary contact\n let contactIsPrimary = r.values.IsPrimary\n console.log(contactIsPrimary)\n if (contactIsPrimary === '1') {\n \n //Add the contact\n ContactId = r.recordID\n SuppliedName = r.values.FirstName + ' ' + r.values.LastName\n SuppliedEmail = r.values.Email\n SuppliedPhone = r.values.Phone\n }\n })\n \n return Compose.makeRecord({\n 'OwnerId' : $record.values.OwnerId,\n 'Subject' : '(no subject)',\n 'ContactId' : ContactId,\n 'AccountId' : $record.recordID,\n 'Status' : 'New',\n 'Priority' : 'Low',\n 'SuppliedName' : SuppliedName,\n 'SuppliedEmail' : SuppliedEmail,\n 'SuppliedPhone' : SuppliedPhone,\n 'CaseNumber' : nextCaseNumber\n }, 'Case').then(myCase => {\n \n Compose.saveRecord(myCase)\n \n }).then(mySavedCase => {\n let nextCaseNumberUpdated = parseInt(nextCaseNumber,10) + 1\n \n // Update the config\n defaultSettings.values.CaseNextNumber = nextCaseNumberUpdated\n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n \n console.log('Record saved, new ID', mySavedDefaultSettings.recordID)\n \n }).then(({ mySavedCase }) => {\n \n // Notify current user\n ComposeUI.success(`The new case has been created.`)\n \n \n }).then(({ mySavedCase }) => {\n \n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedCase)\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n ContactCreateNewCase:\n source: |-\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n \n // Map the case number\n let nextCaseNumber = defaultSettings.values.CaseNextNumber\n if (typeof nextCaseNumber == \"undefined\" || nextCaseNumber === '' || isNaN(nextCaseNumber)) {\n nextCaseNumber = 0\n }\n \n return Compose.makeRecord({\n 'OwnerId' : $record.values.OwnerId,\n 'Subject' : '(no subject)',\n 'ContactId' : $record.recordID,\n 'AccountId' : $record.values.AccountId,\n 'Status' : 'New',\n 'Priority' : 'Low',\n 'SuppliedName' : $record.values.FirstName + ' ' + $record.values.LastName,\n 'SuppliedEmail' : $record.values.Email,\n 'SuppliedPhone' : $record.values.Phone,\n 'CaseNumber' : nextCaseNumber\n }, 'Case').then(myCase => {\n \n // return was missing here. Set tjhe mySavedCase below\n return Compose.saveRecord(myCase)\n \n }).then(mySavedCase => {\n let nextCaseNumberUpdated = parseInt(nextCaseNumber, 10) + 1\n \n // Update the config\n defaultSettings.values.CaseNextNumber = nextCaseNumberUpdated\n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n \n console.log('Record saved, new ID', mySavedDefaultSettings.recordID)\n // then(({ mySavedCase }) wasn't needed, since it is already defined\n // for the entire block, starting at line 27\n }).then(() => {\n\n // Notify current user\n ComposeUI.success(`The new case has been created.`)\n \n }).then(() => {\n\n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedCase)\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n LeadConvertLeadIntoAccount:\n source: |-\n if ($record.values.Status === 'Converted') {\n // Lead already converted. Inform user and exit\n ComposeUI.warning('This lead is already converted.')\n return true\n }\n\n // create new record of type/module Account and copy all values\n return Compose.makeRecord({\n 'BillingStreet' : $record.values.Street,\n 'BillingCity' : $record.values.City,\n 'BillingState' : $record.values.State,\n 'BillingPostalCode' : $record.values.PostalCode,\n 'BillingCountry' : $record.values.Country,\n 'AnnualRevenue' : $record.values.AnnualRevenue,\n 'AccountName' : $record.values.Company,\n 'Description' : $record.values.Description,\n 'Fax' : $record.values.Fax,\n 'Industry' : $record.values.Industry,\n 'OwnerId' : $record.values.OwnerId,\n 'AccountSource' : $record.values.LeadSource,\n 'Phone' : $record.values.Phone,\n 'NumberOfEmployees' : $record.values.NumberOfEmployees,\n 'Rating' : $record.values.Rating,\n 'Website' : $record.values.Website,\n 'Twitter' : $record.values.Twitter,\n 'Facebook' : $record.values.Facebook,\n 'LinkedIn' : $record.values.LinkedIn\n }, 'Account').then(myAccount => {\n \n return Compose.saveRecord(myAccount)\n \n }).then(mySavedAccount => {\n\n // Create the related contact\n return Compose.makeRecord({\n 'MailingStreet' : $record.values.Street,\n 'MailingCity' : $record.values.City,\n 'MailingState' : $record.values.State,\n 'MailingPostalCode' : $record.values.PostalCode,\n 'MailingCountry' : $record.values.Country,\n 'Description' : $record.values.Description,\n 'DoNotCall' : $record.values.DoNotCall,\n 'Email' : $record.values.Email,\n 'HasOptedOutOfEmail' : $record.values.HasOptedOutOfEmail,\n 'Fax' : $record.values.Fax,\n 'HasOptedOutOfFax' : $record.values.HasOptedOutOfFax,\n 'OwnerId' : $record.values.OwnerId,\n 'LeadSource' : $record.values.LeadSource,\n 'Website' : $record.values.Website,\n 'Twitter' : $record.values.Twitter,\n 'Facebook' : $record.values.Facebook,\n 'LinkedIn' : $record.values.LinkedIn,\n 'Salutation' : $record.values.Salutation,\n 'FirstName' : $record.values.FirstName,\n 'LastName' : $record.values.LastName,\n 'MobilePhone' : $record.values.MobilePhone,\n 'Phone' : $record.values.Phone,\n 'Title' : $record.values.Title,\n 'IsPrimary' : '1',\n 'AccountId' : mySavedAccount.recordID\n }, 'Contact').then(myContact => {\n \n return Compose.saveRecord(myContact)\n \n }).then(updatedAccount => {\n \n //Update the lead record\n $record.values.Status = 'Converted'\n $record.values.IsConverted = 'Yes'\n $record.values.ConvertedAccountId = mySavedAccount.recordID\n $record.values.ConvertedContactId = mySavedAccount.recordID\n $record.values.ConvertedDate = mySavedAccount.createdAt\n \n return Compose.saveRecord($record)\n \n }).then(leadUser => {\n \n return System.findUserByID($record.values.OwnerId).then(user => {\n \n // Notifies the owner that a new account was created and assigned to him\n Compose.sendRecordToMail(\n user.email,\n 'Lead ' + $record.values.FirstName + ' ' + $record.values.LastName + ' from ' + $record.values.Company + ' has been converted',\n {\n header: '

The following lead has been converted:

'\n },\n mySavedAccount\n )\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n }).then(({ mySavedAccount }) => {\n \n // Notify current user\n ComposeUI.success(`The lead has been converted.`)\n \n \n }).then(({ mySavedAccount }) => {\n \n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedAccount)\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n LeadConvertLeadToAccountAndOpportunity:\n source: |-\n if ($record.values.Status === 'Converted') {\n // Lead already converted. Inform user and exit\n ComposeUI.warning('This lead is already converted.')\n return true\n }\n\n // create new record of type/module Account and copy all values\n return Compose.makeRecord({\n 'BillingStreet' : $record.values.Street,\n 'BillingCity' : $record.values.City,\n 'BillingState' : $record.values.State,\n 'BillingPostalCode' : $record.values.PostalCode,\n 'BillingCountry' : $record.values.Country,\n 'AnnualRevenue' : $record.values.AnnualRevenue,\n 'AccountName' : $record.values.Company,\n 'Description' : $record.values.Description,\n 'Fax' : $record.values.Fax,\n 'Industry' : $record.values.Industry,\n 'OwnerId' : $record.values.OwnerId,\n 'AccountSource' : $record.values.LeadSource,\n 'Phone' : $record.values.Phone,\n 'NumberOfEmployees' : $record.values.NumberOfEmployees,\n 'Rating' : $record.values.Rating,\n 'Website' : $record.values.Website,\n 'Twitter' : $record.values.Twitter,\n 'Facebook' : $record.values.Facebook,\n 'LinkedIn' : $record.values.LinkedIn\n }, 'Account').then(myAccount => {\n \n return Compose.saveRecord(myAccount)\n \n }).then(mySavedAccount => {\n\n // Create the related contact\n return Compose.makeRecord({\n 'MailingStreet' : $record.values.Street,\n 'MailingCity' : $record.values.City,\n 'MailingState' : $record.values.State,\n 'MailingPostalCode' : $record.values.PostalCode,\n 'MailingCountry' : $record.values.Country,\n 'Description' : $record.values.Description,\n 'DoNotCall' : $record.values.DoNotCall,\n 'Email' : $record.values.Email,\n 'HasOptedOutOfEmail' : $record.values.HasOptedOutOfEmail,\n 'Fax' : $record.values.Fax,\n 'HasOptedOutOfFax' : $record.values.HasOptedOutOfFax,\n 'OwnerId' : $record.values.OwnerId,\n 'LeadSource' : $record.values.LeadSource,\n 'Website' : $record.values.Website,\n 'Twitter' : $record.values.Twitter,\n 'Facebook' : $record.values.Facebook,\n 'LinkedIn' : $record.values.LinkedIn,\n 'Salutation' : $record.values.Salutation,\n 'FirstName' : $record.values.FirstName,\n 'LastName' : $record.values.LastName,\n 'MobilePhone' : $record.values.MobilePhone,\n 'Phone' : $record.values.Phone,\n 'Title' : $record.values.Title,\n 'IsPrimary' : '1',\n 'AccountId' : mySavedAccount.recordID\n }, 'Contact').then(mySavedContact => {\n \n return Compose.saveRecord(mySavedContact).then(mySavedContact => {\n \n // First get the default values\n //Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n \n let opportunityCloseDays = defaultSettings.values.OpportunityCloseDateDays\n let opportunityProbability = defaultSettings.values.OpportunityProbability\n let opportunityForecaseCategory = defaultSettings.values.OpportunityForecaseCategory\n let opportunityStagename = defaultSettings.values.OpportunityStagename\n \n //Calculate the expiration date\n let m = new Date()\n m.setDate(m.getDate() + parseInt(opportunityCloseDays,10))\n let closeDate = m.getUTCFullYear() +\"/\"+ (m.getUTCMonth()+1) +\"/\"+ m.getUTCDate() + \" \" + m.getUTCHours() + \":\" + m.getUTCMinutes() + \":\" + m.getUTCSeconds()\n \n // Create the related opportunity\n return Compose.makeRecord({\n 'Description' : $record.values.Description,\n 'OwnerId' : $record.values.OwnerId,\n 'LeadSource' : $record.values.LeadSource,\n 'Name' : '(unnamed)',\n 'AccountId' : mySavedAccount.recordID,\n 'IsClosed' : 'No',\n 'IsWon' : 'No',\n 'CloseDate' : closeDate,\n 'Probability' : opportunityProbability,\n 'ForecastCategory' : opportunityForecaseCategory,\n 'StageName' : opportunityStagename\n }, 'Opportunity').then(myOpportunity => {\n return Compose.saveRecord(myOpportunity)\n \n }).then(mySavedOpportunity => {\n \n //Create a new contact linked to the opportunity\n return Compose.makeRecord({\n 'ContactId' : mySavedContact.recordID,\n 'OpportunityId' : mySavedOpportunity.recordID,\n 'IsPrimary' : '1'\n }, 'OpportunityContactRole').then(myOpportunityContactRole => {\n return Compose.saveRecord(myOpportunityContactRole)\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n \n \n }).then(updatedAccount => {\n \n //Update the lead record\n $record.values.Status = 'Converted'\n $record.values.IsConverted = 'Yes'\n $record.values.ConvertedAccountId = mySavedAccount.recordID\n $record.values.ConvertedContactId = mySavedAccount.recordID\n $record.values.ConvertedDate = mySavedAccount.createdAt\n \n return Compose.saveRecord($record)\n \n }).then(leadUser => {\n \n return System.findUserByID($record.values.OwnerId).then(user => {\n \n // Notifies the owner that a new account was created and assigned to him\n Compose.sendRecordToMail(\n user.email,\n 'Lead ' + $record.values.FirstName + ' ' + $record.values.LastName + ' from ' + $record.values.Company + ' has been converted',\n {\n header: '

The following lead has been converted:

'\n },\n mySavedAccount\n )\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n }).then(({ mySavedAccount }) => {\n \n // Notify current user\n ComposeUI.success(`The lead has been converted.`)\n \n \n }).then(({ mySavedAccount }) => {\n \n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedAccount)\n \n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n QuoteSendQuoteToCustomEmail:\n source: |-\n // Get the to address\n let to = prompt(\"Please enter an email to send this quote to:\")\n if (to === null || to === \"\") {\n ComposeUI.warning(`Please enter an email to send this quote to .`)\n return\n }\n\n let lineitems = ''\n Compose.findRecords(`QuoteId = ${$record.recordID}`, 'QuoteLineItem')\n\n .then(({ set, filter }) => {\n\n set.forEach(lineitem => {\n\n lineitems = lineitems + \"Price: \" + lineitem.values.UnitPrice + \"
\"\n lineitems = lineitems + \"Quantity: \" + lineitem.values.Quantity + \"
\"\n lineitems = lineitems + \"Subtotal: \" + lineitem.values.Subtotal + \"
\"\n lineitems = lineitems + \"Discount: \" + lineitem.values.Discount + \"
\"\n lineitems = lineitems + \"Total Price: \" + lineitem.values.TotalPrice + \"
\"\n lineitems = lineitems + \"-------------------------
\" \n })\n return lineitems\n }).then(lineitems => {\n\n\n // Get email body\n let html = \"

Details of Quote: \" + $record.values.QuoteNumber + \" - \" + $record.values.Name + \"

\"\n html = html + \"
\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"
\"\n html = html + \"Quote Information
\"\n html = html + \"
\"\n html = html + \"Quote Number: \" + $record.values.QuoteNumber + \"
\"\n html = html + \"Quote Name: \" + $record.values.Name + \"
\"\n html = html + \"Expiration Date: \" + $record.values.ExpirationDate + \"
\"\n html = html + \"Description: \" + $record.values.Description\n html = html + \"
\"\n html = html + \"Primary contact data
\"\n html = html + \"
\"\n html = html + \"Contact Name: \" + $record.values.ContactId + \"
\"\n html = html + \"Email: \" + $record.values.Email + \"
\"\n html = html + \"Phone: \" + $record.values.Phone\n html = html + \"
\"\n html = html + \"Totals
\"\n html = html + \"
\"\n html = html + \"Subtotal: \" + $record.values.Subtotal + \"
\"\n html = html + \"Additional Discount: \" + $record.values.Discount + \"
\"\n html = html + \"Shipping and Handling: \" + $record.values.ShippingHandling + \"
\"\n html = html + \"Total Price: \" + $record.values.TotalPrice + \"
\"\n html = html + \"Tax: \" + $record.values.Tax + \"
\"\n html = html + \"Grand Total: \" + $record.values.GrandTotal\n html = html + \"
\"\n html = html + \"Products
\"\n html = html + \"
\"\n html = html + lineitems\n html = html + \"
\"\n html = html + \"Bill To
\"\n html = html + \"
\"\n html = html + \"Bill to Name: \" + $record.values.BillingName + \"
\"\n html = html + \"Bill to Street: \" + $record.values.BillingStreet + \"
\"\n html = html + \"Bill to City: \" + $record.values.BillingCity + \"
\"\n html = html + \"Bill to State: \" + $record.values.BillingState + \"
\"\n html = html + \"Bill to Postal Code: \" + $record.values.BillingPostalCode + \"
\"\n html = html + \"Bill to Country: \" + $record.values.BillingCountry\n html = html + \"
\"\n html = html + \"Quote To
\"\n html = html + \"
\"\n html = html + \"Quote to Name: \" + $record.values.QuoteToName + \"
\"\n html = html + \"Quote to Street: \" + $record.values.QuoteToStreet + \"
\"\n html = html + \"Quote to City: \" + $record.values.QuoteToCity + \"
\"\n html = html + \"Quote to State: \" + $record.values.QuoteToState + \"
\"\n html = html + \"Quote to Postal Code: \" + $record.values.QuoteToPostalCode + \"
\"\n html = html + \"Quote to Country: \" + $record.values.QuoteToCountry\n html = html + \"
\"\n html = html + \"Ship To
\"\n html = html + \"
\"\n html = html + \"Ship to Name: \" + $record.values.ShippingName + \"
\"\n html = html + \"Ship to Street: \" + $record.values.ShippingStreet + \"
\"\n html = html + \"Ship to City: \" + $record.values.ShippingCity + \"
\"\n html = html + \"Ship to State: \" + $record.values.ShippingState + \"
\"\n html = html + \"Ship to Postal Code: \" + $record.values.ShippingPostalCode + \"
\"\n html = html + \"Ship to Country: \" + $record.values.ShippingCountry\n html = html + \"
\"\n\n // Send the email\n return Compose.sendMail(to, `Corteza - Quote: ${$record.values.QuoteNumber} - ${$record.values.Name}`, { html: html }).then(() => {\n ComposeUI.success(`The quote has been sent via email.`)\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n QuoteSendQuoteToPrimaryContact:\n source: |-\n // Get the to address\n let to = $record.values.Email\n if (!to) {\n ComposeUI.warning(`There is no email linked to the quote. Please fill in an email address in the \"Primary contact data\" block.`)\n return\n }\n\n let lineitems = ''\n Compose.findRecords(`QuoteId = ${$record.recordID}`, 'QuoteLineItem')\n\n .then(({ set, filter }) => {\n\n set.forEach(lineitem => {\n\n lineitems = lineitems + \"Price: \" + lineitem.values.UnitPrice + \"
\"\n lineitems = lineitems + \"Quantity: \" + lineitem.values.Quantity + \"
\"\n lineitems = lineitems + \"Subtotal: \" + lineitem.values.Subtotal + \"
\"\n lineitems = lineitems + \"Discount: \" + lineitem.values.Discount + \"
\"\n lineitems = lineitems + \"Total Price: \" + lineitem.values.TotalPrice + \"
\"\n lineitems = lineitems + \"-------------------------
\" \n })\n return lineitems\n }).then(lineitems => {\n\n\n // Get email body\n let html = \"

Details of Quote: \" + $record.values.QuoteNumber + \" - \" + $record.values.Name + \"

\"\n html = html + \"
\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"\"\n html = html + \"
\"\n html = html + \"Quote Information
\"\n html = html + \"
\"\n html = html + \"Quote Number: \" + $record.values.QuoteNumber + \"
\"\n html = html + \"Quote Name: \" + $record.values.Name + \"
\"\n html = html + \"Expiration Date: \" + $record.values.ExpirationDate + \"
\"\n html = html + \"Description: \" + $record.values.Description\n html = html + \"
\"\n html = html + \"Primary contact data
\"\n html = html + \"
\"\n html = html + \"Contact Name: \" + $record.values.ContactId + \"
\"\n html = html + \"Email: \" + $record.values.Email + \"
\"\n html = html + \"Phone: \" + $record.values.Phone\n html = html + \"
\"\n html = html + \"Totals
\"\n html = html + \"
\"\n html = html + \"Subtotal: \" + $record.values.Subtotal + \"
\"\n html = html + \"Additional Discount: \" + $record.values.Discount + \"
\"\n html = html + \"Shipping and Handling: \" + $record.values.ShippingHandling + \"
\"\n html = html + \"Total Price: \" + $record.values.TotalPrice + \"
\"\n html = html + \"Tax: \" + $record.values.Tax + \"
\"\n html = html + \"Grand Total: \" + $record.values.GrandTotal\n html = html + \"
\"\n html = html + \"Products
\"\n html = html + \"
\"\n html = html + lineitems\n html = html + \"
\"\n html = html + \"Bill To
\"\n html = html + \"
\"\n html = html + \"Bill to Name: \" + $record.values.BillingName + \"
\"\n html = html + \"Bill to Street: \" + $record.values.BillingStreet + \"
\"\n html = html + \"Bill to City: \" + $record.values.BillingCity + \"
\"\n html = html + \"Bill to State: \" + $record.values.BillingState + \"
\"\n html = html + \"Bill to Postal Code: \" + $record.values.BillingPostalCode + \"
\"\n html = html + \"Bill to Country: \" + $record.values.BillingCountry\n html = html + \"
\"\n html = html + \"Quote To
\"\n html = html + \"
\"\n html = html + \"Quote to Name: \" + $record.values.QuoteToName + \"
\"\n html = html + \"Quote to Street: \" + $record.values.QuoteToStreet + \"
\"\n html = html + \"Quote to City: \" + $record.values.QuoteToCity + \"
\"\n html = html + \"Quote to State: \" + $record.values.QuoteToState + \"
\"\n html = html + \"Quote to Postal Code: \" + $record.values.QuoteToPostalCode + \"
\"\n html = html + \"Quote to Country: \" + $record.values.QuoteToCountry\n html = html + \"
\"\n html = html + \"Ship To
\"\n html = html + \"
\"\n html = html + \"Ship to Name: \" + $record.values.ShippingName + \"
\"\n html = html + \"Ship to Street: \" + $record.values.ShippingStreet + \"
\"\n html = html + \"Ship to City: \" + $record.values.ShippingCity + \"
\"\n html = html + \"Ship to State: \" + $record.values.ShippingState + \"
\"\n html = html + \"Ship to Postal Code: \" + $record.values.ShippingPostalCode + \"
\"\n html = html + \"Ship to Country: \" + $record.values.ShippingCountry\n html = html + \"
\"\n\n // Send the email\n return Compose.sendMail(to, `Corteza - Quote: ${$record.values.QuoteNumber} - ${$record.values.Name}`, { html: html }).then(() => {\n ComposeUI.success(`The quote has been sent via email.`)\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n QuoteSubmitQuoteForApproval:\n source: |-\n // Check if it can be reviewed\n if ($record.values.Status !== 'Draft' && $record.values.Status !== 'Needs Review') {\n crust.notify.ui.alert.warning('A quote needs to have the status Draft or Needs Review in order to be sent for approval')\n return true\n }\n\n // Set the status\n $record.values.Status = 'In Review'\n\n // Saves the quote and inform\n return Compose.saveRecord($record)\n .then(mySavedRecord => {\n\n // Get the email of the owner\n return System.findUserByID($record.createdBy).then(user => {\n\n // Send the mail\n return Compose.sendMail(\n user.email, //Change this to the email address of the person that needs to approve the quotes\n `Quote \"${$record.values.Name}\" needs approval`,\n { html: `The following quote needs approval:

${$record.values.Name}` }\n )\n }).then(() => {\n\n // Notify current user\n ComposeUI.success(`The quote has been sent for approval.`)\n\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n OpportunityGenerateNewQuote:\n source: |-\n // Check if there is a related account, to map the fields of the account\n if (!$record.values.AccountId) {\n\n // Exit when there is no account related to the opportunity.\n ComposeUI.warning('Please link the opportunity to an account before generating a quote')\n return\n }\n\n let quoteContactId\n let quoteEmail\n let quotePhone\n let quoteFax\n let quoteAccountId\n let quoteBillingStreet\n let quoteBillingCity\n let quoteBillingState\n let quoteBillingPostalCode\n let quoteBillingCountry\n let quoteBillingName\n let quoteToStreet\n let quoteToCity\n let quoteToState\n let quoteToPostalCode\n let quoteToCountry\n let quoteToName\n let quoteShippingStreet\n let quoteShippingCity\n let quoteShippingState\n let quoteShippingPostalCode\n let quoteShippingCountry\n let quoteShippingName\n let quoteExpirationDate\n let quoteNumber\n\n // Get the primary contact for the quote\n return Compose.findRecords(`OpportunityId = ${$record.recordID}`, 'OpportunityContactRole')\n\n .then(({ set, filter }) => {\n\n let primary_contact\n\n if (set.length === 1) {\n\n // Get the contact\n primary_contact = set[0]\n\n } else {\n\n // Loop through the contacts of the account, to save the primary contact\n set.forEach(r => {\n\n // Check if it's the primary contact\n let contactIsPrimary = r.values.IsPrimary\n if (contactIsPrimary === '1') {\n\n // Add the contact\n primary_contact = r\n }\n })\n }\n return primary_contact\n\n }).then(primary_contact => {\n\n // If we have the primary contact, continue to add it to the quote. Else, skip this block\n if (primary_contact) {\n\n // Get the contact data\n return Compose.findRecordByID(primary_contact.values.ContactId, 'Contact').then(contact => {\n\n quoteContactId = contact.recordID\n quoteEmail = contact.values.Email\n quotePhone = contact.values.Phone\n quoteFax = contact.values.Fax\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }\n\n }).then(mySavedQuote => {\n\n // Get the related account\n return Compose.findRecordByID($record.values.AccountId, 'Account').then(account => {\n\n quoteAccountId = account.recordID,\n quoteBillingStreet = account.values.BillingStreet,\n quoteBillingCity = account.values.BillingCity,\n quoteBillingState = account.values.BillingState,\n quoteBillingPostalCode = account.values.BillingPostalCode,\n quoteBillingCountry = account.values.BillingCountry,\n quoteBillingName = account.values.AccountName,\n quoteToStreet = account.values.BillingStreet,\n quoteToCity = account.values.BillingCity,\n quoteToState = account.values.BillingState,\n quoteToPostalCode = account.values.BillingPostalCode,\n quoteToCountry = account.values.BillingCountry,\n quoteToName = account.values.AccountName,\n quoteShippingStreet = account.values.BillingStreet,\n quoteShippingCity = account.values.BillingCity,\n quoteShippingState = account.values.BillingState,\n quoteShippingPostalCode = account.values.BillingPostalCode,\n quoteShippingCountry = account.values.BillingCountry,\n quoteShippingName = account.values.AccountName\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n }).then(mySavedQuote => {\n\n // Get the default settings\n return Compose.findRecordByID('96976273003249666', 'DefaultSettings').then(defaultSettings => {\n\n // Get the expiration date\n let quoteExpirationDays = defaultSettings.values.QuoteExpirationDays\n\n // Calculate the expiration date\n let m = new Date()\n m.setDate(m.getDate() + parseInt(quoteExpirationDays, 10))\n let expirationDate = m.getUTCFullYear() + \"/\" + (m.getUTCMonth() + 1) + \"/\" + m.getUTCDate() + \" \" + m.getUTCHours() + \":\" + m.getUTCMinutes() + \":\" + m.getUTCSeconds()\n\n // Save the date\n quoteExpirationDate = expirationDate\n\n // Map the quote number\n let nextQuoteNumber = defaultSettings.values.QuoteNextNumber\n if (typeof nextQuoteNumber == \"undefined\" || nextQuoteNumber === '' || isNaN(nextQuoteNumber)) {\n nextQuoteNumber = 0\n }\n quoteNumber = nextQuoteNumber\n let nextQuoteNumberUpdated = parseInt(nextQuoteNumber, 10) + 1\n\n // Update the config\n defaultSettings.values.QuoteNextNumber = nextQuoteNumberUpdated\n return Compose.saveRecord(defaultSettings).then(mySavedDefaultSettings => {\n\n console.log('Record saved, ID', mySavedDefaultSettings.recordID)\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n }).then(mySavedQuote => {\n\n // Create a new quote record for the opportunity\n return Compose.makeRecord({\n 'ShippingHandling': 0,\n 'Status': 'Draft',\n 'Discount': 0,\n 'Tax': 0,\n 'OpportunityId': $record.recordID,\n 'GrandTotal': $record.values.Amount,\n 'PricebookId': $record.values.PricebookId,\n 'Name': $record.values.Name,\n 'Subtotal': $record.values.Amount,\n 'TotalPrice': $record.values.Amount,\n 'ContactId': quoteContactId,\n 'Email': quoteEmail,\n 'Phone': quotePhone,\n 'Fax': quoteFax,\n 'AccountId': quoteAccountId,\n 'BillingStreet': quoteBillingStreet,\n 'BillingCity': quoteBillingCity,\n 'BillingState': quoteBillingState,\n 'BillingPostalCode': quoteBillingPostalCode,\n 'BillingCountry': quoteBillingCountry,\n 'BillingName': quoteBillingName,\n 'QuoteToStreet': quoteToStreet,\n 'QuoteToCity': quoteToCity,\n 'QuoteToState': quoteToState,\n 'QuoteToPostalCode': quoteToPostalCode,\n 'QuoteToCountry': quoteToCountry,\n 'QuoteToName': quoteToName,\n 'ShippingStreet': quoteShippingStreet,\n 'ShippingCity': quoteShippingCity,\n 'ShippingState': quoteShippingState,\n 'ShippingPostalCode': quoteShippingPostalCode,\n 'ShippingCountry': quoteShippingCountry,\n 'ShippingName': quoteShippingName,\n 'ExpirationDate': quoteExpirationDate,\n 'QuoteNumber': quoteNumber\n }, 'Quote')\n\n .then(myQuote => {\n\n return Compose.saveRecord(myQuote)\n\n .then(mySavedQuote => {\n\n // Get the list of products from the opportunity to the quote\n return Compose.findRecords(`OpportunityId = ${$record.recordID}`, 'OpportunityLineItem')\n\n .then(({ set, filter }) => {\n\n // Loop through the lineitems related to the opportunity\n set.forEach(r => {\n\n // Create a new contact linked to the opportunity\n return Compose.makeRecord({\n 'Discount': r.values.Discount,\n 'Description': r.values.Description,\n 'ListPrice': r.values.ListPrice,\n 'PricebookEntryId': r.values.PricebookEntryId,\n 'ProductId': r.values.ProductId,\n 'ProductCode': r.values.ProductCode,\n 'Quantity': r.values.Quantity,\n 'UnitPrice': r.values.UnitPrice,\n 'Subtotal': r.values.Subtotal,\n 'TotalPrice': r.values.TotalPrice,\n 'QuoteId': mySavedQuote.recordID\n }, 'QuoteLineItem')\n\n .then(myQuoteLineItem => {\n\n return Compose.saveRecord(myQuoteLineItem)\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n })\n })\n\n }).then(() => {\n\n // Notify current user\n ComposeUI.success(`The new quote has been created.`)\n\n\n }).then(() => {\n\n // Go to the record\n ComposeUI.gotoRecordEditor(mySavedQuote)\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\n QuoteUpdateTotalPrice:\n source: |-\n // Get the subtotal\n let subtotal = parseFloat($record.values.Subtotal)\n\n // Apply additional quote discount\n let discount = $record.values.Discount\n if (!discount || discount === '' || isNaN(discount)) {\n discount = 0\n }\n let totalPrice = subtotal - parseFloat(discount)\n\n // Calculate if it's not below 0\n if (totalPrice < 0) {\n totalPrice = 0\n }\n\n // Apply shipping\n let shippingHandling = $record.values.ShippingHandling\n if (!shippingHandling || shippingHandling === '' || isNaN(shippingHandling)) {\n shippingHandling = 0\n }\n totalPrice = totalPrice + parseFloat(shippingHandling)\n\n // Add totalPrice to the record\n $record.values.TotalPrice = totalPrice\n\n // Apply taxes\n let tax = $record.values.Tax\n if (!shippingHandling || shippingHandling === '' || isNaN(shippingHandling)) {\n // No tax, so don't do anything\n $record.values.GrandTotal = totalPrice\n } else {\n if (tax > 0) {\n // Apply tax\n let taxpercent = parseFloat(tax / 100);\n $record.values.GrandTotal = totalPrice * (1 + taxpercent)\n }\n }\n async: false\n runInUA: false\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: afterUpdate\n module: Quote\n resource: compose:record\n OpportunityApplyPriceBook:\n source: |-\n // Get the current price book\n let pricebookId = $record.values.PricebookId\n\n // Check if there is a price book. If there isn't one, find the standard one\n if (!pricebookId) {\n\n //If there is no price book selected, get the default price book.\n return Compose.findRecords('IsActive = 1', 'Pricebook')\n\n .then(({ set, filter }) => {\n\n if (set.length === 0) {\n\n // return that there are no Price books in the CRM\n ComposeUI.warning(`There are no active price books configured in the CRM. Please insert an active price book in the Price book module.`)\n\n } else {\n\n // Loop through the price books, to find the standard one\n set.forEach(r => {\n\n //Check if the price book is the standard one\n if (r.values.IsStandard === '1') {\n\n //Get the price book id\n pricebookId = r.recordID\n }\n })\n\n if (pricebookId) {\n\n //Save the price book in the opportunity\n $record.values.PricebookId = pricebookId\n\n // Save the price book in the opportunity\n return Compose.saveRecord($record)\n }\n }\n\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }\n\n // Check if a price book is selected or if a standard price book has been found. If not, exit.\n if (!pricebookId) {\n ComposeUI.warning(`Please select a Price book for this opportunity first.`)\n return\n }\n\n //Set the total amount of the opportunity\n let amount = 0\n\n // Find all opportunity lineitems\n return Compose.findRecords(`OpportunityId = ${$record.recordID}`, 'OpportunityLineItem')\n\n .then(({ set, filter }) => {\n\n set.forEach(lineitem => {\n\n //Set the default values\n let quantity = lineitem.values.Quantity\n let discount = lineitem.values.Discount\n let listprice = 0\n let unitprice = 0\n let subtotal = 0\n let totalprice = 0\n\n // Get the product\n return Compose.findRecordByID(lineitem.values.ProductId, 'Product').then(product => {\n\n // Set the product name and code\n lineitem.values.Name = product.values.Name\n lineitem.values.ProductCode = product.values.ProductCode\n\n }).then(product => {\n\n // Get the right price from the selected price book\n return Compose.findRecords(`PricebookId = ${pricebookId} AND ProductId = ${lineitem.values.ProductId}`, 'PricebookEntry')\n\n .then(({ set, filter }) => {\n\n if (set.length > 0) {\n\n let pricebookEntry = set[0]\n\n // Get the list price\n listprice = pricebookEntry.values.UnitPrice\n\n // Update unitprice only when the value is empty\n unitprice = lineitem.values.UnitPrice\n if (!unitprice || unitprice === '' || isNaN(unitprice)) {\n unitprice = listprice\n }\n\n // Calculate the totals\n if (!quantity || quantity === '' || isNaN(quantity)) {\n quantity = 0\n }\n subtotal = unitprice * quantity\n\n // Calculate the total\n if (!discount || discount === '' || isNaN(discount)) {\n totalprice = subtotal\n } else {\n totalprice = subtotal - discount\n }\n\n // Update it in the listitem record\n lineitem.values.ListPrice = listprice\n lineitem.values.UnitPrice = unitprice\n lineitem.values.Subtotal = subtotal\n lineitem.values.TotalPrice = totalprice\n\n //Add the total price to the amount of the opportunity\n amount = amount + totalprice\n\n // Save the lineitem\n return Compose.saveRecord(lineitem)\n }\n }).then(mySolution => {\n\n //Save the opportunity record\n $record.values.Amount = amount\n return Compose.saveRecord($record)\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n }).catch(err => {\n // solve the problem\n console.error(err)\n })\n async: false\n runInUA: true\n critical: true\n enabled: true\n timeout: 0\n triggers:\n - event: manual\n resource: compose:record\nPK\x07\x08\x19\xef\xf6B\xc4\x0c\x01\x00\xc4\x0c\x01\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x001400_pages.yamlUT\x05\x00\x01\x80Cm8namespace: crm\npages:\n - handle: Home\n title: Home\n blocks:\n - title: New Leads\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Email\n - name: LeadSource\n - name: Status\n module: Lead\n page: RecordPageForModuleLead\n perPage: 5\n prefilter: Status = 'New'\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 14, 6, 9]\n - title: Leads by Type\n options:\n chart: LeadsByType\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [9, 0, 3, 7]\n - title: Open Opportunities\n options:\n fields:\n - name: Name\n - name: AccountId\n - name: Amount\n - name: StageName\n - name: NextStep\n module: Opportunity\n page: RecordPageForModuleOpportunity\n perPage: 3\n prefilter: StageName != 'Closed Won' AND StageName != 'Closed Lost'\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [6, 14, 6, 9]\n - title: Leads by Country\n options:\n chart: LeadsByCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [6, 0, 3, 7]\n - title: Leads per day\n options:\n chart: LeadsPerDay\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [6, 7, 3, 7]\n - title: My Calendar\n options:\n feeds:\n - endField: EndDateTime\n module: Event\n startField: ActivityDate\n titleField: Subject\n - endField: null\n module: Task\n startField: ActivityDate\n titleField: Subject\n header:\n views:\n - agendaWeek\n - agendaMonth\n - agendaDay\n - month\n - listMonth\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Calendar\n xywh: [0, 0, 6, 14]\n - title: Opportunities\n options:\n chart: OpportunitiesByValue\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [9, 7, 3, 7]\n visible: true\n - handle: Accounts\n title: Accounts\n blocks:\n - title: All accounts\n options:\n fields:\n - name: AccountName\n - name: Type\n - name: Rating\n - name: Industry\n - name: AccountSource\n - name: OwnerId\n - name: createdAt\n module: Account\n page: RecordPageForModuleAccount\n perPage: 20\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 0, 12, 19]\n pages:\n - handle: RecordPageForModuleAccount\n module: Account\n title: Record page for module \"Account\"\n blocks:\n - title: Account Detail\n options:\n fields:\n - name: AccountName\n - name: Type\n - name: Industry\n - name: NumberOfEmployees\n - name: AnnualRevenue\n - name: Twitter\n - name: LinkedIn\n - name: Facebook\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 3, 12]\n - title: Account Address\n options:\n fields:\n - name: BillingStreet\n - name: BillingCity\n - name: BillingState\n - name: BillingPostalCode\n - name: BillingCountry\n - name: Phone\n - name: Website\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [3, 0, 3, 12]\n - title: Other Information\n options:\n fields:\n - name: Rating\n - name: ParentId\n - name: AccountSource\n - name: OwnerId\n - name: Description\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [6, 0, 3, 12]\n - title: Company Twitter Feed\n options:\n fields: []\n profileSourceField: Twitter\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: SocialFeed\n xywh: [9, 0, 3, 20]\n - title: Opportunities\n options:\n fields:\n - name: Name\n - name: StageName\n - name: Amount\n - name: CloseDate\n hideAddButton: true\n module: Opportunity\n page: RecordPageForModuleOpportunity\n perPage: 5\n prefilter: AccountId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 12, 6, 8]\n - title: Contacts\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Title\n hidePaging: true\n module: Contact\n page: RecordPageForModuleContact\n perPage: 5\n prefilter: AccountId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 20, 4, 8]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: AccountId = ${recordID}\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 28, 4, 8]\n - title: Cases\n options:\n fields:\n - name: CaseNumber\n - name: Subject\n - name: Status\n hideAddButton: true\n module: Case\n page: RecordPageForModuleCase\n perPage: 5\n prefilter: AccountId = ${recordID}\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [8, 20, 4, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: AccountId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [4, 28, 4, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: AccountId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [8, 28, 4, 8]\n - title: Contracts\n options:\n fields:\n - name: Name\n - name: StartDate\n - name: EndDate\n - name: Status\n hideAddButton: true\n module: Contract\n page: RecordPageForModuleContract\n perPage: 5\n prefilter: AccountId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [4, 20, 4, 8]\n - title: Account Actions\n options:\n buttons:\n - label: Generate a new Opportunity for this Account\n script: AccountCreateNewOpportunity\n variant: primary\n - label: Generate a new Contract for this Account\n script: AccountCreateNewContract\n variant: primary\n - label: Generate a new Case for this Account\n script: AccountCreateNewCase\n variant: primary\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Automation\n xywh: [6, 12, 3, 8]\n pages:\n - handle: RecordPageForModuleAccountContactRole\n module: AccountContactRole\n title: Record page for module \"AccountContactRole\"\n blocks:\n - title: Account Contact Role Details\n options:\n fields:\n - name: AccountId\n - name: ContactId\n - name: Role\n - name: IsPrimary\n kind: Record\n xywh: [0, 0, 5, 15]\n visible: false\n visible: true\n - handle: MyAccounts\n title: My Accounts\n visible: true\n visible: true\n - handle: Leads\n title: Leads\n blocks:\n - title: List of leads\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Email\n - name: Rating\n - name: Company\n - name: Country\n - name: Status\n - name: OwnerId\n module: Lead\n page: RecordPageForModuleLead\n perPage: 20\n prefilter: \"\"\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 0, 9, 21]\n - title: Leads by type\n options:\n chart: LeadsByType\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [9, 0, 3, 7]\n - title: Leads by country\n options:\n chart: LeadsByCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [9, 7, 3, 7]\n - title: Leads by source\n options:\n chart: LeadsBySource\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Chart\n xywh: [9, 14, 3, 7]\n pages:\n - handle: RecordPageForModuleLead\n module: Lead\n title: Record page for module \"Lead\"\n blocks:\n - title: Personal Data\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Salutation\n - name: Title\n - name: Phone\n - name: MobilePhone\n - name: Email\n - name: Website\n - name: Twitter\n - name: LinkedIn\n - name: Facebook\n kind: Record\n xywh: [0, 0, 3, 15]\n - title: Twitter feed\n options:\n fields: []\n profileSourceField: Twitter\n kind: SocialFeed\n xywh: [9, 0, 3, 31]\n - title: Company Data\n options:\n fields:\n - name: Company\n - name: Industry\n - name: AnnualRevenue\n - name: NumberOfEmployees\n - name: NumberOfEmployeesRange\n - name: Country\n - name: City\n - name: Street\n - name: State\n - name: PostalCode\n kind: Record\n xywh: [3, 0, 3, 15]\n - title: Other Information\n options:\n fields:\n - name: Rating\n - name: Status\n - name: LeadSource\n - name: OwnerId\n - name: Description\n - name: ConvertedAccountId\n - name: ConvertedContactId\n - name: ConvertedOpportunityId\n - name: ConvertedDate\n kind: Record\n xywh: [6, 0, 3, 15]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: LeadId = ${recordID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 15, 6, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: LeadId = ${recordID}\n presort: ActivityDate DESC\n kind: RecordList\n xywh: [0, 23, 4, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: LeadId = ${recordID}\n presort: ActivityDate DESC\n kind: RecordList\n xywh: [4, 23, 5, 8]\n - title: Lead Actions\n options:\n buttons:\n - label: Convert this Lead in to an Account\n script: LeadConvertLeadIntoAccount\n variant: primary\n - label: 'Lead: Convert a lead into an account and opportunity'\n script: LeadConvertLeadToAccountAndOpportunity\n variant: primary\n kind: Automation\n xywh: [6, 15, 3, 8]\n visible: true\n - handle: AllNewLeads\n title: All New Leads\n blocks:\n - title: New Leads\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Email\n - name: Rating\n - name: Company\n - name: Country\n - name: LeadSource\n - name: Status\n - name: OwnerId\n module: Lead\n page: RecordPageForModuleLead\n perPage: 20\n prefilter: Status = 'New' OR Status = 'Assigned'\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n visible: true\n - handle: MyLeads\n title: My Leads\n blocks:\n - title: My Leads\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Email\n - name: Rating\n - name: Company\n - name: Country\n - name: LeadSource\n - name: Status\n - name: OwnerId\n module: Lead\n page: RecordPageForModuleLead\n perPage: 20\n prefilter: OwnerId = ${userID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 21]\n visible: true\n visible: true\n - handle: Opportunities\n title: Opportunities\n blocks:\n - title: List of Opportunities\n options:\n fields:\n - name: Name\n - name: Amount\n - name: AccountId\n - name: StageName\n - name: CloseDate\n - name: OwnerId\n - name: createdAt\n module: Opportunity\n page: RecordPageForModuleOpportunity\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleOpportunity\n module: Opportunity\n title: Record page for module \"Opportunity\"\n blocks:\n - title: Opportunity Detail\n options:\n fields:\n - name: Name\n - name: AccountId\n - name: Type\n - name: CloseDate\n - name: Probability\n - name: PricebookId\n - name: Amount\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 4, 11]\n - title: Additional Information\n options:\n fields:\n - name: OwnerId\n - name: NextStep\n - name: LeadSource\n - name: CampaignId\n - name: ForecastCategory\n - name: Description\n - name: createdAt\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [8, 0, 4, 11]\n - title: Contact Roles\n options:\n fields:\n - name: ContactId\n - name: IsPrimary\n - name: Role\n module: OpportunityContactRole\n page: RecordPageForModuleOpportunityContactRole\n perPage: 5\n prefilter: OpportunityId = ${recordID}\n presort: IsPrimary DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [6, 19, 6, 8]\n - title: Opportunity Qualification\n options:\n fields:\n - name: StageName\n - name: Budget\n - name: Authority\n - name: Need\n - name: Timeline\n - name: Competition\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [4, 0, 4, 11]\n - title: Products\n options:\n fields:\n - name: ProductId\n - name: ProductCode\n - name: ListPrice\n - name: UnitPrice\n - name: Quantity\n - name: Subtotal\n - name: Discount\n - name: TotalPrice\n module: OpportunityLineItem\n page: RecordPageForModuleOpportunityLineItem\n perPage: 5\n prefilter: OpportunityId = ${recordID}\n presort: ProductId ASC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 11, 8, 8]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: OpportunityId = ${recordID}\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 27, 4, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: OpportunityId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [4, 27, 4, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: OpportunityId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [8, 27, 4, 8]\n - title: Quotes\n options:\n fields:\n - name: QuoteNumber\n - name: Name\n - name: ExpirationDate\n - name: Subtotal\n - name: TotalPrice\n hideAddButton: true\n module: Quote\n page: RecordPageForModuleQuote\n perPage: 5\n prefilter: OpportunityId = ${recordID}\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 19, 6, 8]\n - title: Opportunity Actions\n options:\n buttons:\n - label: Update prices of Products by applying the selected Price Book\n and inserted discounts\n script: OpportunityApplyPriceBook\n variant: primary\n - label: Generate a new Quote for this Opportunity\n script: OpportunityGenerateNewQuote\n variant: primary\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Automation\n xywh: [8, 11, 4, 8]\n pages:\n - handle: RecordPageForModuleOpportunityLineItem\n module: OpportunityLineItem\n title: Record page for module \"OpportunityLineItem\"\n blocks:\n - title: Opportunity Line Item Details\n options:\n fields:\n - name: OpportunityId\n - name: ProductId\n - name: Description\n - name: UnitPrice\n - name: Quantity\n - name: Discount\n kind: Record\n xywh: [0, 0, 4, 13]\n - title: Automatically inserted values\n options:\n fields:\n - name: ProductCode\n - name: ListPrice\n - name: Subtotal\n - name: TotalPrice\n kind: Record\n xywh: [4, 0, 4, 13]\n visible: false\n - handle: RecordPageForModuleOpportunityCompetitor\n module: OpportunityCompetitor\n title: Record page for module \"OpportunityCompetitor\"\n blocks:\n - title: Opportunity Competitor Details\n options:\n fields:\n - name: OpportunityId\n - name: CompetitorName\n - name: Strengths\n - name: Weaknesses\n kind: Record\n xywh: [0, 0, 5, 17]\n visible: false\n - handle: RecordPageForModuleOpportunityContactRole\n module: OpportunityContactRole\n title: Record page for module \"OpportunityContactRole\"\n blocks:\n - title: Opportunity Contact Role Details\n options:\n fields:\n - name: OpportunityId\n - name: ContactId\n - name: Role\n - name: IsPrimary\n kind: Record\n xywh: [0, 0, 4, 16]\n pages:\n - handle: RecordPageForModulePricebookEntry\n module: PricebookEntry\n title: Record page for module \"PricebookEntry\"\n blocks:\n - title: Details\n options:\n fields:\n - name: PricebookId\n - name: ProductId\n - name: UnitPrice\n kind: Record\n xywh: [0, 0, 4, 15]\n visible: false\n visible: false\n visible: false\n - handle: MyOpportunities\n title: My Opportunities\n blocks:\n - title: My Opportunities\n options:\n fields:\n - name: Name\n - name: Amount\n - name: AccountId\n - name: StageName\n - name: CloseDate\n - name: OwnerId\n - name: createdAt\n module: Opportunity\n page: RecordPageForModuleOpportunity\n perPage: 20\n prefilter: OwnerId = ${userID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n visible: true\n visible: true\n - handle: Contacts\n title: Contacts\n blocks:\n - title: List of Contacts\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: AccountId\n - name: Phone\n - name: Email\n - name: OwnerId\n module: Contact\n page: RecordPageForModuleContact\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleContact\n module: Contact\n title: Record page for module \"Contact\"\n blocks:\n - title: Personal Data\n options:\n fields:\n - name: FirstName\n - name: LastName\n - name: Salutation\n - name: Title\n - name: Phone\n - name: Email\n - name: Twitter\n - name: Facebook\n - name: LinkedIn\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 3, 13]\n - title: Contact Address\n options:\n fields:\n - name: AccountId\n - name: IsPrimary\n - name: MailingStreet\n - name: MailingCity\n - name: MailingState\n - name: MailingPostalCode\n - name: MailingCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [3, 0, 3, 13]\n - title: Other Information\n options:\n fields:\n - name: OwnerId\n - name: DoNotCall\n - name: HasOptedOutOfEmail\n - name: ContactSource\n - name: Description\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [6, 0, 3, 13]\n - title: Contact Twitter Feed\n options:\n fields: []\n profileSourceField: Twitter\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: SocialFeed\n xywh: [9, 0, 3, 20]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: LastModifiedDate\n - name: CreatedById\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: ContactId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 21, 4, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: ContactId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [4, 21, 4, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: ContactId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [8, 21, 4, 8]\n - title: Cases related with this Contact\n options:\n fields:\n - name: CaseNumber\n - name: Status\n - name: Subject\n - name: createdAt\n hideAddButton: true\n module: Case\n page: RecordPageForModuleCase\n perPage: 5\n prefilter: ContactId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 13, 6, 8]\n - title: Contact Actions\n options:\n buttons:\n - label: Create a new Case for this Contact\n script: ContactCreateNewCase\n variant: primary\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Automation\n xywh: [6, 13, 3, 8]\n visible: false\n visible: true\n - handle: Quotes\n title: Quotes\n blocks:\n - title: List of Quotes\n options:\n fields:\n - name: QuoteNumber\n - name: Name\n - name: OpportunityId\n - name: Discount\n - name: TotalPrice\n - name: Tax\n - name: GrandTotal\n - name: createdAt\n - name: ExpirationDate\n module: Quote\n page: RecordPageForModuleQuote\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleQuote\n module: Quote\n title: Record page for module \"Quote\"\n blocks:\n - title: Quote Information\n options:\n fields:\n - name: QuoteNumber\n - name: Name\n - name: OpportunityId\n - name: ExpirationDate\n - name: Status\n - name: Description\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 4, 12]\n - title: Totals\n options:\n fields:\n - name: PricebookId\n - name: Subtotal\n - name: Discount\n - name: ShippingHandling\n - name: TotalPrice\n - name: Tax\n - name: GrandTotal\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [8, 0, 4, 12]\n - title: Primary contact data\n options:\n fields:\n - name: AccountId\n - name: ContactId\n - name: Email\n - name: Phone\n - name: createdBy\n - name: createdAt\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [4, 0, 4, 12]\n - title: Bill To\n options:\n fields:\n - name: BillingName\n - name: BillingStreet\n - name: BillingCity\n - name: BillingState\n - name: BillingPostalCode\n - name: BillingCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 20, 4, 9]\n - title: Quote To\n options:\n fields:\n - name: QuoteToName\n - name: QuoteToStreet\n - name: QuoteToCity\n - name: QuoteToState\n - name: QuoteToPostalCode\n - name: QuoteToCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [4, 20, 4, 9]\n - title: Ship To\n options:\n fields:\n - name: ShippingName\n - name: ShippingStreet\n - name: ShippingCity\n - name: ShippingState\n - name: ShippingPostalCode\n - name: ShippingCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [8, 20, 4, 9]\n - title: Quote Line Items\n options:\n fields:\n - name: ProductId\n - name: UnitPrice\n - name: Quantity\n - name: Subtotal\n - name: Discount\n - name: TotalPrice\n - name: ListPrice\n module: QuoteLineItem\n page: RecordPageForModuleQuoteLineItem\n perPage: 5\n prefilter: QuoteId = ${recordID}\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 12, 8, 8]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: QuoteId = ${recordID}\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 29, 4, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: QuoteId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [4, 29, 4, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: QuoteId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [8, 29, 4, 8]\n - title: Quote Actions\n options:\n buttons:\n - label: Submit this Quote for approval\n script: QuoteSubmitQuoteForApproval\n variant: primary\n - label: Approve this Quote\n script: QuoteApproveQuote\n variant: primary\n - label: Send this Quote via email to the primary contact\n script: QuoteSendQuoteToPrimaryContact\n variant: primary\n - label: Send this Quote to a custom email\n script: QuoteSendQuoteToCustomEmail\n variant: primary\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Automation\n xywh: [8, 12, 4, 8]\n pages:\n - handle: RecordPageForModuleQuoteLineItem\n module: QuoteLineItem\n title: Record page for module \"QuoteLineItem\"\n blocks:\n - title: Quote Line Item Information\n options:\n fields:\n - name: QuoteId\n - name: LineItemNumber\n - name: LineItemDescription\n - name: ProductId\n - name: ListPrice\n - name: UnitPrice\n - name: Quantity\n - name: Subtotal\n - name: Discount\n - name: TotalPrice\n kind: Record\n xywh: [0, 0, 4, 19]\n visible: false\n visible: false\n visible: true\n - handle: Products\n title: Products\n blocks:\n - title: List of Products\n options:\n fields:\n - name: Name\n - name: ProductCode\n - name: Description\n - name: Family\n module: Product\n page: RecordPageForModuleProduct\n perPage: 20\n presort: Name ASC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleProduct\n module: Product\n title: Record page for module \"Product\"\n blocks:\n - title: Product Details\n options:\n fields:\n - name: Name\n - name: ProductCode\n - name: Family\n - name: Description\n - name: IsActive\n kind: Record\n xywh: [0, 0, 6, 10]\n - title: Appears in the following Price Books\n options:\n fields:\n - name: PricebookId\n - name: UnitPrice\n hideAddButton: true\n module: PricebookEntry\n page: RecordPageForModulePricebookEntry\n perPage: 5\n prefilter: ProductId = ${recordID}\n kind: RecordList\n xywh: [6, 0, 6, 10]\n - title: Related Cases\n options:\n fields:\n - name: CaseNumber\n - name: Subject\n - name: Status\n hideAddButton: true\n module: Case\n page: RecordPageForModuleCase\n perPage: 5\n prefilter: ProductId = ${recordID}\n presort: CaseNumber DESC\n kind: RecordList\n xywh: [0, 10, 6, 8]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: ProductId = ${recordID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [6, 10, 6, 8]\n visible: false\n - handle: PriceBook\n title: Price book\n blocks:\n - title: Price books\n options:\n fields:\n - name: Name\n - name: Description\n - name: IsStandard\n - name: IsActive\n module: Pricebook\n page: RecordPageForModulePricebook\n perPage: 20\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleCaseContactRole\n module: CaseContactRole\n title: Record page for module \"CaseContactRole\"\n blocks:\n - title: Case Contact Role Details\n options:\n fields:\n - name: CasesId\n - name: ContactId\n - name: Role\n - name: IsPrimary\n kind: Record\n xywh: [0, 0, 5, 16]\n visible: false\n - handle: RecordPageForModulePricebook\n module: Pricebook\n title: Record page for module \"Pricebook\"\n blocks:\n - title: Price Book Details\n options:\n fields:\n - name: Name\n - name: Description\n - name: IsStandard\n - name: IsActive\n kind: Record\n xywh: [0, 0, 6, 14]\n - title: Price Book Entries\n options:\n fields:\n - name: ProductId\n - name: UnitPrice\n module: PricebookEntry\n page: RecordPageForModulePricebookEntry\n perPage: 12\n prefilter: PricebookId = ${recordID}\n kind: RecordList\n xywh: [6, 0, 6, 14]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: PricebookId = ${recordID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 14, 12, 8]\n visible: false\n visible: true\n visible: true\n - handle: Contracts\n title: Contracts\n blocks:\n - title: List of Contracts\n options:\n fields:\n - name: ContractNumber\n - name: AccountId\n - name: Status\n - name: StartDate\n - name: EndDate\n - name: ContractTerm\n module: Contract\n page: RecordPageForModuleContract\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleContract\n module: Contract\n title: Record page for module \"Contract\"\n blocks:\n - title: Contract Details\n options:\n fields:\n - name: AccountId\n - name: ContractNumber\n - name: Status\n - name: StartDate\n - name: EndDate\n - name: ContractTerm\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 4, 12]\n - title: Contract Billing Address\n options:\n fields:\n - name: BillingStreet\n - name: BillingCity\n - name: BillingState\n - name: BillingPostalCode\n - name: BillingCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 12, 4, 9]\n - title: Contract Description and Special Terms\n options:\n fields:\n - name: OwnerId\n - name: Description\n - name: SpecialTerms\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [4, 0, 4, 12]\n - title: Contract Files\n options:\n fields:\n - name: File1\n - name: File2\n - name: File3\n - name: File4\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [8, 0, 4, 12]\n - title: Contract Shipping Address\n options:\n fields:\n - name: ShippingStreet\n - name: ShippingCity\n - name: ShippingState\n - name: ShippingPostalCode\n - name: ShippingCountry\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [4, 12, 4, 9]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: ContractId = ${recordID}\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 21, 4, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: ContractId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [4, 21, 4, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: ContractId = ${recordID}\n presort: ActivityDate DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [8, 21, 4, 8]\n pages:\n - handle: RecordPageForModuleContractLineItem\n module: ContractLineItem\n title: Record page for module \"ContractLineItem\"\n blocks:\n - title: Contract Line Item Details\n options:\n fields:\n - name: LineItemNumber\n - name: Description\n - name: StartDate\n - name: EndDate\n - name: Status\n kind: Record\n xywh: [0, 0, 4, 11]\n - title: Contract Line Item Pricing\n options:\n fields:\n - name: UnitPrice\n - name: Quantity\n - name: Subtotal\n - name: Discount\n - name: TotalPrice\n kind: Record\n xywh: [4, 0, 4, 11]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: ContractLineItemId = ${recordID}\n kind: RecordList\n xywh: [8, 0, 4, 11]\n visible: false\n - handle: RecordPageForModuleEntitlement\n module: Entitlement\n title: Record page for module \"Entitlement\"\n blocks:\n - title: Entitlement Details\n options:\n fields:\n - name: Name\n - name: Entitlement Process\n - name: CasesPerEntitlement\n - name: Type\n - name: RemainingCases\n - name: IsPerIncident\n - name: StartDate\n - name: EndDate\n - name: Status\n kind: Record\n xywh: [0, 0, 4, 13]\n - title: Other Information\n options:\n fields:\n - name: AccountId\n - name: ContractLineItemId\n - name: CreatedById\n - name: CreatedDate\n - name: LastModifiedById\n - name: LastModifiedDate\n kind: Record\n xywh: [4, 0, 4, 13]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: EntitlementId = ${recordID}\n kind: RecordList\n xywh: [6, 13, 6, 7]\n - title: Entitlement Templates\n options:\n fields:\n - name: Name\n - name: Type\n - name: CasesPerEntitlement\n - name: IsPerIncident\n - name: Term\n module: EntitlementTemplate\n page: RecordPageForModuleEntitlementTemplate\n perPage: 5\n prefilter: EntitlementId = ${recordID}\n kind: RecordList\n xywh: [0, 13, 6, 7]\n - title: Entitlement Contacts\n options:\n fields:\n - name: ContactId\n - name: Name\n module: EntitlementContact\n page: RecordPageForModuleEntitlementContact\n perPage: 8\n prefilter: EntitlementId = ${recordID}\n kind: RecordList\n xywh: [8, 0, 4, 13]\n pages:\n - handle: RecordPageForModuleEntitlementTemplate\n module: EntitlementTemplate\n title: Record page for module \"EntitlementTemplate\"\n blocks:\n - title: Entitlement Template Details\n options:\n fields:\n - name: Name\n - name: Type\n - name: CasesPerEntitlement\n - name: IsPerIncident\n - name: Term\n - name: BusinessHours\n kind: Record\n xywh: [0, 0, 4, 14]\n visible: false\n - handle: RecordPageForModuleEntitlementContact\n module: EntitlementContact\n title: Record page for module \"EntitlementContact\"\n blocks:\n - title: Entitlement Contact Details\n options:\n fields:\n - name: EntitlementId\n - name: ContactId\n - name: Name\n kind: Record\n xywh: [0, 0, 4, 14]\n visible: false\n visible: false\n - handle: RecordPageForModuleContractContactRole\n module: ContractContactRole\n title: Record page for module \"ContractContactRole\"\n blocks:\n - title: Contract Contact Role Details\n options:\n fields:\n - name: ContractId\n - name: ContactId\n - name: Role\n - name: IsPrimary\n kind: Record\n xywh: [0, 0, 4, 16]\n visible: false\n visible: false\n visible: true\n - handle: Cases\n title: Cases\n blocks:\n - title: List of Cases\n options:\n fields:\n - name: CaseNumber\n - name: Subject\n - name: Status\n - name: CreatedDate\n - name: OwnerId\n - name: createdAt\n module: Case\n page: RecordPageForModuleCase\n perPage: 20\n presort: createdAt DESC\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleCase\n module: Case\n title: Record page for module \"Case\"\n blocks:\n - title: Case Main Information\n options:\n fields:\n - name: CaseNumber\n - name: Subject\n - name: AccountId\n - name: ContactId\n - name: SuppliedName\n - name: SuppliedEmail\n - name: SuppliedPhone\n kind: Record\n xywh: [0, 0, 3, 14]\n - title: Case Details\n options:\n fields:\n - name: Type\n - name: Description\n - name: File\n - name: Origin\n - name: ProductId\n kind: Record\n xywh: [3, 0, 3, 14]\n - title: Case Status\n options:\n fields:\n - name: Status\n - name: OwnerId\n - name: CreatedDate\n - name: Priority\n - name: IsEscalated\n - name: IsClosed\n - name: ClosedDate\n - name: Reason\n kind: Record\n xywh: [9, 0, 3, 14]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: CaseId = ${recordID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 22, 4, 8]\n - title: Events\n options:\n fields:\n - name: Type\n - name: Subject\n - name: ActivityDate\n module: Event\n page: RecordPageForModuleEvent\n perPage: 5\n prefilter: CaseId = ${recordID}\n presort: ActivityDate DESC\n kind: RecordList\n xywh: [4, 22, 5, 8]\n - title: Tasks\n options:\n fields:\n - name: Type\n - name: Subject\n - name: Status\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 5\n prefilter: CaseId = ${recordID}\n presort: ActivityDate DESC\n kind: RecordList\n xywh: [9, 22, 3, 8]\n - title: Case Updates\n options:\n fields:\n - name: Type\n - name: Description\n - name: File\n - name: createdBy\n - name: createdAt\n module: CaseUpdate\n page: RecordPageForModuleCaseUpdate\n perPage: 5\n prefilter: CaseId = ${recordID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 14, 9, 8]\n - title: Case Actions\n options:\n buttons:\n - label: Set Case status to Working\n script: CaseSetStatusToWorking\n variant: primary\n - label: Set Case status to Closed\n script: CaseSetStatusToClosed\n variant: primary\n - label: Inform the contact via email of the Case's solution\n script: CaseInformContactOfSolution\n variant: primary\n kind: Automation\n xywh: [9, 14, 3, 8]\n - title: Case Solution\n options:\n fields:\n - name: SolutionName\n - name: SolutionNote\n - name: SolutionFile\n - name: SolutionId\n - name: SubmitAsSolution\n kind: Record\n xywh: [6, 0, 3, 14]\n pages:\n - handle: RecordPageForModuleCaseUpdate\n module: CaseUpdate\n title: Record page for module \"CaseUpdate\"\n blocks:\n - title: Case Update\n options:\n fields:\n - name: CaseId\n - name: Description\n - name: Type\n - name: File\n - name: IsSolution\n - name: createdBy\n - name: createdAt\n kind: Record\n xywh: [0, 0, 4, 20]\n visible: false\n - handle: RecordPageForModuleCaseMilestone\n module: CaseMilestone\n title: Record page for module \"CaseMilestone\"\n blocks:\n - title: Case Milestone Details\n options:\n fields:\n - name: CaseId\n - name: TargetDate\n - name: TargetResponseInDays\n - name: TargetResponseInHrs\n - name: TargetResponseInMins\n - name: StartDate\n - name: CompletionDate\n - name: IsViolated\n - name: IsCompleted\n kind: Record\n xywh: [0, 0, 5, 19]\n pages:\n - handle: RecordPageForModuleMilestoneType\n module: MilestoneType\n title: Record page for module \"MilestoneType\"\n blocks:\n - title: Case Milestone Type Details\n options:\n fields:\n - name: Name\n - name: Description\n - name: RecurrenceType\n kind: Record\n xywh: [0, 0, 4, 15]\n visible: false\n visible: false\n visible: false\n - handle: Solutions\n title: Solutions\n blocks:\n - title: List of Solutions\n options:\n fields:\n - name: SolutionNumber\n - name: SolutionName\n - name: SolutionNote\n - name: CaseId\n - name: ProductId\n module: Solution\n page: RecordPageForModuleSolution\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleSolution\n module: Solution\n title: Record page for module \"Solution\"\n blocks:\n - title: Solution Details\n options:\n fields:\n - name: SolutionNumber\n - name: SolutionName\n - name: SolutionNote\n - name: File\n - name: CaseId\n - name: ProductId\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 5, 17]\n visible: false\n visible: true\n visible: true\n - handle: Campaigns\n title: Campaigns\n blocks:\n - title: List of Campaigns\n options:\n fields:\n - name: Name\n - name: ParentId\n - name: Type\n - name: Status\n - name: StartDate\n - name: EndDate\n module: Campaigns\n page: RecordPageForModuleCampaign\n perPage: 20\n prefilter: \"\"\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleCampaign\n module: Campaigns\n title: Record page for module \"Campaign\"\n blocks:\n - title: Campaign Details\n options:\n fields:\n - name: Name\n - name: IsActive\n - name: ParentId\n - name: Type\n - name: OwnerId\n - name: Status\n - name: StartDate\n - name: EndDate\n kind: Record\n xywh: [0, 0, 4, 13]\n - title: Campaign Planning\n options:\n fields:\n - name: BudgetedCost\n - name: ActualCost\n - name: ExpectedRevenue\n - name: CreatedById\n - name: CreatedDate\n - name: Description\n kind: Record\n xywh: [4, 0, 4, 13]\n - title: Campaign Statistics\n options:\n fields:\n - name: NumberOfLeads\n - name: NumberOfConvertedLeads\n - name: NumberOfContacts\n - name: NumberOfOpportunities\n - name: NumberOfWonOpportunities\n - name: AmountAllOpportunities\n - name: AmountWonOpportunities\n kind: Record\n xywh: [8, 0, 4, 13]\n - title: Campaign Members\n options:\n fields:\n - name: LeadId\n - name: ContactId\n - name: Status\n module: CampaignMember\n page: RecordPageForModuleCampaignMember\n perPage: 5\n prefilter: CampaignId = ${recordID}\n kind: RecordList\n xywh: [0, 13, 8, 8]\n - title: Generated Opportunities\n options:\n fields:\n - name: Name\n - name: StageName\n - name: Amount\n - name: CloseDate\n module: Opportunity\n page: RecordPageForModuleOpportunity\n perPage: 5\n prefilter: CampaignId = ${recordID}\n kind: RecordList\n xywh: [0, 21, 8, 8]\n - title: Notes\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n module: Note\n page: RecordPageForModuleNote\n perPage: 5\n prefilter: CampaignId = ${recordID}\n presort: createdAt DESC\n kind: RecordList\n xywh: [8, 21, 4, 8]\n pages:\n - handle: RecordPageForModuleCampaignMember\n module: CampaignMember\n title: Record page for module \"CampaignMember\"\n blocks:\n - title: Campaign Member Details\n options:\n fields:\n - name: CampaignId\n - name: LeadId\n - name: ContactId\n - name: HasResponded\n - name: FirstRespondedDate\n - name: Status\n kind: Record\n xywh: [0, 0, 5, 13]\n pages:\n - handle: RecordPageForModuleCampaignMemberStatus\n module: 'Error: module with ID 69055788664029201 does not exist'\n title: Record page for module \"CampaignMemberStatus\"\n visible: false\n visible: false\n visible: false\n visible: true\n - handle: Tasks\n title: Tasks\n blocks:\n - title: List of Tasks\n options:\n fields:\n - name: Subject\n - name: Status\n - name: LeadId\n - name: AccountId\n - name: ContactId\n - name: OpportunityId\n - name: QuoteId\n - name: ContractId\n - name: CaseId\n - name: ActivityDate\n module: Task\n page: RecordPageForModuleTask\n perPage: 20\n prefilter: \"\"\n presort: ActivityDate DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleTask\n module: Task\n title: Record page for module \"Task\"\n blocks:\n - title: Task Details\n options:\n fields:\n - name: Subject\n - name: Description\n - name: Type\n kind: Record\n xywh: [0, 0, 4, 20]\n - title: Related To\n options:\n fields:\n - name: LeadId\n - name: AccountId\n - name: ContactId\n - name: OpportunityId\n - name: QuoteId\n - name: ContractId\n kind: Record\n xywh: [8, 0, 4, 20]\n - title: Task Status\n options:\n fields:\n - name: Status\n - name: ActivityDate\n - name: Priority\n - name: createdBy\n - name: createdAt\n kind: Record\n xywh: [4, 0, 4, 20]\n visible: false\n visible: true\n - handle: Events\n title: Events\n blocks:\n - title: List of Events\n options:\n fields:\n - name: WhoId\n - name: Subject\n - name: ActivityDate\n - name: StartDateTime\n - name: EndDateTime\n - name: LeadId\n - name: AccountId\n - name: OpportunityId\n - name: createdBy\n module: Event\n page: RecordPageForModuleEvent\n perPage: 20\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleEvent\n module: Event\n title: Record page for module \"Event\"\n blocks:\n - title: Event Data\n options:\n fields:\n - name: Subject\n - name: Description\n - name: Location\n - name: createdAt\n - name: createdBy\n kind: Record\n xywh: [0, 0, 4, 20]\n - title: Related Objects\n options:\n fields:\n - name: LeadId\n - name: AccountId\n - name: ContactId\n - name: OpportunityId\n - name: QuoteId\n - name: ContractId\n - name: CaseId\n kind: Record\n xywh: [8, 0, 4, 20]\n - title: Event Date and Time\n options:\n fields:\n - name: ActivityDate\n - name: DurationInMinutes\n - name: EndDateTime\n - name: IsAllDayEvent\n kind: Record\n xywh: [4, 0, 4, 20]\n visible: false\n visible: true\n - handle: Notes\n title: Notes\n blocks:\n - title: List of Notes\n options:\n fields:\n - name: Title\n - name: LeadId\n - name: AccountId\n - name: ContactId\n - name: OpportunityId\n - name: QuoteId\n - name: ContractId\n - name: CaseId\n - name: createdAt\n - name: createdBy\n module: Note\n page: RecordPageForModuleNote\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleNote\n module: Note\n title: Record page for module \"Note\"\n blocks:\n - title: Note\n options:\n fields:\n - name: Title\n - name: Body\n - name: createdAt\n - name: createdBy\n kind: Record\n xywh: [0, 0, 4, 20]\n - title: Files\n options:\n fields:\n - name: FileAdd1\n - name: FileAdd2\n - name: FileAdd3\n - name: FileAdd4\n kind: Record\n xywh: [4, 0, 4, 20]\n - title: Related Objects\n options:\n fields:\n - name: LeadId\n - name: AccountId\n - name: ContactId\n - name: OpportunityId\n - name: QuoteId\n - name: ContractId\n - name: CaseId\n kind: Record\n xywh: [8, 0, 4, 20]\n visible: false\n visible: true\n - handle: Dashboards\n title: Dashboards\n blocks:\n - title: Content\n options:\n body:

Each page in Corteza can be a dashboard. You can add text blocks,\n record lists or charts to make a Sales Activities Dashboard, an executive\n dashboard, or any other dashboard you may need, and set the access\n permissions to define who can view or edit the dashboard.

\n kind: Content\n xywh: [0, 0, 5, 16]\n - title: Leads per day\n options:\n chart: LeadsPerDay\n kind: Chart\n xywh: [5, 0, 3, 16]\n pages:\n - handle: DefaultSettings\n title: Default Settings\n description: A list of defaults values which are called upon in automation\n processes.\n blocks:\n - title: Default settings\n options:\n fields:\n - name: createdAt\n - name: updatedAt\n hidePaging: true\n module: DefaultSettings\n page: RecordPageForModuleDefaultSettings\n perPage: 1\n kind: RecordList\n xywh: [0, 0, 12, 7]\n pages:\n - handle: RecordPageForModuleDefaultSettings\n module: DefaultSettings\n title: Record page for module \"DefaultSettings\"\n blocks:\n - title: Quote Settings\n options:\n fields:\n - name: QuoteNextNumber\n - name: QuoteExpirationDays\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [3, 0, 3, 12]\n - title: Opportunity Settings\n options:\n fields:\n - name: OpportunityCloseDateDays\n - name: OpportunityProbability\n - name: OpportunityForecaseCategory\n - name: OpportunityStagename\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [0, 0, 3, 12]\n - title: Contract Settings\n options:\n fields:\n - name: ContractNextNumber\n - name: ContractDefaultTime\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [6, 0, 3, 12]\n - title: Case Settings\n options:\n fields:\n - name: CaseNextNumber\n - name: SolutionNextNumber\n style:\n variants: {bodyBg: white, border: primary, headerBg: white, headerText: primary}\n kind: Record\n xywh: [9, 0, 3, 12]\n visible: false\n visible: true\n visible: true\n - handle: UnpublishedModules\n title: Unpublished modules\n pages:\n - handle: Questions\n title: Questions\n visible: false\n blocks:\n - title: List of Questions\n options:\n fields:\n - name: Title\n - name: CreatedDate\n - name: NumReplies\n module: Question\n page: RecordPageForModuleQuestion\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleQuestion\n module: Question\n title: Record page for module \"Question\"\n blocks:\n - title: Question Details\n options:\n fields:\n - name: Title\n - name: Body\n - name: NumReplies\n - name: BestReplyId\n - name: UpVotes\n - name: VoteScore\n kind: Record\n xywh: [0, 0, 4, 13]\n - title: Replies\n options:\n fields:\n - name: Body\n - name: Name\n - name: UpVotes\n - name: DownVotes\n - name: VoteTotal\n module: Reply\n page: RecordPageForModuleReply\n perPage: 5\n prefilter: QuestionId = ${recordID}\n kind: RecordList\n xywh: [0, 13, 12, 7]\n - title: Votes\n options:\n fields:\n - name: Score\n module: Vote\n page: RecordPageForModuleVote\n perPage: 8\n prefilter: QuestionId = ${recordID}\n kind: RecordList\n xywh: [4, 0, 8, 13]\n pages:\n - handle: RecordPageForModuleVote\n module: Vote\n title: Record page for module \"Vote\"\n blocks:\n - title: Vote Score\n options:\n fields:\n - name: Score\n - name: CreatedById\n - name: CreatedDate\n kind: Record\n xywh: [0, 0, 4, 13]\n - title: Related Object\n options:\n fields:\n - name: IdeaId\n - name: ReplyId\n - name: QuestionId\n kind: Record\n xywh: [4, 0, 4, 13]\n visible: false\n - handle: RecordPageForModuleReply\n module: Reply\n title: Record page for module \"Reply\"\n blocks:\n - title: Reply Details\n options:\n fields:\n - name: QuestionId\n - name: IdeaId\n - name: Body\n - name: Name\n - name: UpVotes\n - name: DownVotes\n - name: VoteTotal\n kind: Record\n xywh: [0, 0, 6, 20]\n - title: Votes\n options:\n fields:\n - name: Score\n - name: CreatedById\n - name: CreatedDate\n module: Vote\n page: RecordPageForModuleVote\n perPage: 20\n prefilter: ReplyId = ${recordID}\n kind: RecordList\n xywh: [6, 0, 6, 20]\n visible: false\n visible: false\n - handle: Ideas\n title: Ideas\n blocks:\n - title: List of Ideas\n options:\n fields:\n - name: Title\n - name: Categories\n - name: ParentIdeaId\n - name: VoteScore\n - name: Status\n module: Idea\n page: RecordPageForModuleIdea\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleIdea\n module: Idea\n title: Record page for module \"Idea\"\n blocks:\n - title: Idea Details\n options:\n fields:\n - name: Title\n - name: Body\n - name: Categories\n - name: ParentIdeaId\n - name: CreatorName\n - name: Status\n - name: VoteScore\n - name: VoteTotal\n kind: Record\n xywh: [0, 0, 4, 20]\n - title: Votes\n options:\n fields:\n - name: Score\n - name: CreatedById\n - name: CreatedDate\n module: Vote\n page: RecordPageForModuleVote\n perPage: 20\n prefilter: IdeaId = ${recordID}\n kind: RecordList\n xywh: [8, 0, 4, 20]\n - title: Replies\n options:\n fields:\n - name: Body\n - name: Name\n - name: UpVotes\n - name: DownVotes\n - name: VoteTotal\n module: Reply\n page: RecordPageForModuleReply\n perPage: 20\n prefilter: IdeaId = ${recordID}\n kind: RecordList\n xywh: [4, 0, 4, 20]\n visible: false\n visible: false\n - handle: Emails\n title: Emails\n blocks:\n - title: List of Email Templates\n options:\n fields:\n - name: Name\n - name: IsActive\n - name: createdBy\n module: EmailTemplate\n page: RecordPageForModuleEmailTemplate\n perPage: 20\n presort: createdAt DESC\n kind: RecordList\n xywh: [0, 0, 12, 20]\n pages:\n - handle: RecordPageForModuleEmailTemplate\n module: EmailTemplate\n title: Record page for module \"EmailTemplate\"\n blocks:\n - title: Email Template Details\n options:\n fields:\n - name: Name\n - name: Subject\n - name: Description\n - name: Body\n - name: IsActive\n kind: Record\n xywh: [0, 0, 6, 19]\n visible: false\n visible: false\n visible: false\nPK\x07\x082\\e\xc4\xa1\x1f\x01\x00\xa1\x1f\x01\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf6e\x19/dev/null 2>&1 && pwd )" function download { SRC="https://raw.githubusercontent.com/cortezaproject/corteza-configs/master/${1}" @@ -13,26 +14,9 @@ function getCrmConfig { NAMES="1000_namespace 1100_modules 1200_charts 1300_scripts 1400_pages" for NAME in $NAMES; do - download "crm/${NAME}.yaml" "./compose/${NAME}.yaml" + download "crm/${NAME}.yaml" "./compose/src/${NAME}.yaml" done } -function get { - getCrmConfig -} +getCrmConfig -function gen { - echo "generating..." -} - -case ${1:-"all"} in - gen) - gen - ;; - get) - get - ;; - all) - get - gen -esac diff --git a/system/auth/external/register.go b/system/auth/external/register.go index c4f909353..3e39e9346 100644 --- a/system/auth/external/register.go +++ b/system/auth/external/register.go @@ -115,7 +115,7 @@ func RegisterOidcProvider(ctx context.Context, name, providerUrl string, force, } if as.ExternalRedirectUrl == "" { - return nil, errors.New("refusing to register oidc privider withoit redirect url") + return nil, errors.New("refusing to register OIDC provider without redirect url") } p, err := parseExternalProviderUrl(providerUrl) diff --git a/system/commands/importer.go b/system/commands/importer.go index 925c1baab..609586ea3 100644 --- a/system/commands/importer.go +++ b/system/commands/importer.go @@ -6,14 +6,10 @@ import ( "os" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" "github.com/cortezaproject/corteza-server/internal/auth" - "github.com/cortezaproject/corteza-server/internal/permissions" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/system/importer" - "github.com/cortezaproject/corteza-server/system/service" - "github.com/cortezaproject/corteza-server/system/types" ) func Importer(ctx context.Context, c *cli.Config) *cobra.Command { @@ -26,7 +22,6 @@ func Importer(ctx context.Context, c *cli.Config) *cobra.Command { c.InitServices(ctx, c) var ( - aux interface{} ff []io.Reader err error ) @@ -39,33 +34,10 @@ func Importer(ctx context.Context, c *cli.Config) *cobra.Command { ff[a], err = os.Open(arg) cli.HandleError(err) } + cli.HandleError(importer.Import(ctx, ff...)) } else { - args = []string{"STDIN"} - ff = []io.Reader{os.Stdin} + cli.HandleError(importer.Import(ctx, os.Stdin)) } - - roles, err := service.DefaultRole.With(ctx).Find(&types.RoleFilter{}) - cli.HandleError(err) - - perm := permissions.NewImporter(service.DefaultAccessControl.Whitelist()) - - imp := importer.NewImporter(perm, - importer.NewRoleImport(perm, roles), - ) - - for i, f := range ff { - cmd.Printf("Importing from %s\n", args[i]) - cli.HandleError(yaml.NewDecoder(f).Decode(&aux)) - - cli.HandleError(imp.Cast(aux)) - } - - cli.HandleError(imp.Store( - ctx, - service.DefaultRole.With(ctx), - service.DefaultAccessControl, - roles, - )) }, } diff --git a/system/commands/users.go b/system/commands/users.go index 1d5ee03a4..ea927a9a4 100644 --- a/system/commands/users.go +++ b/system/commands/users.go @@ -16,6 +16,10 @@ import ( ) func Users(ctx context.Context, c *cli.Config) *cobra.Command { + var ( + flagNoPassword bool + ) + // User management commands. cmd := &cobra.Command{ Use: "users", @@ -82,28 +86,41 @@ func Users(ctx context.Context, c *cli.Config) *cobra.Command { password []byte ) + if existing, _ := userRepo.FindByEmail(user.Email); existing != nil && existing.ID > 0 { + cmd.Printf("User already exists [%d].\n", existing.ID) + return + } + if user, err = userRepo.Create(user); err != nil { cli.HandleError(err) } cmd.Printf("User created [%d].\n", user.ID) - cmd.Print("Set password: ") - if password, err = terminal.ReadPassword(syscall.Stdin); err != nil { - cli.HandleError(err) - } + if !flagNoPassword { + cmd.Print("Set password: ") + if password, err = terminal.ReadPassword(syscall.Stdin); err != nil { + cli.HandleError(err) + } - if len(password) == 0 { - // Password not set, that's ok too. - return - } + if len(password) == 0 { + // Password not set, that's ok too. + return + } - if err = authSvc.SetPassword(user.ID, string(password)); err != nil { - cli.HandleError(err) + if err = authSvc.SetPassword(user.ID, string(password)); err != nil { + cli.HandleError(err) + } } }, } + addCmd.Flags().BoolVar( + &flagNoPassword, + "no-password", + false, + "Create user without password") + pwdCmd := &cobra.Command{ Use: "password [email]", Short: "Change password for user", diff --git a/system/importer/aux.go b/system/importer/aux.go new file mode 100644 index 000000000..3aa90cd44 --- /dev/null +++ b/system/importer/aux.go @@ -0,0 +1,56 @@ +package importer + +import ( + "context" + "io" + + "gopkg.in/yaml.v2" + + "github.com/cortezaproject/corteza-server/internal/permissions" + "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" +) + +// Import performs standard import procedure with default services +func Import(ctx context.Context, ff ...io.Reader) (err error) { + var ( + roles types.RoleSet + aux interface{} + ) + + roles, err = service.DefaultRole.With(ctx).Find(&types.RoleFilter{}) + if err != nil { + return err + } + + pi := permissions.NewImporter(service.DefaultAccessControl.Whitelist()) + imp := NewImporter( + pi, + NewRoleImport(pi, roles), + ) + + for _, f := range ff { + if err = yaml.NewDecoder(f).Decode(&aux); err != nil { + return + } + + err = imp.Cast(aux) + if err != nil { + return + } + } + + // Get roles across the system + // roles, err := service.DefaultSystemRole.Find(ctx) + // if err != nil { + // return + // } + + // Store all imported + return imp.Store( + ctx, + service.DefaultRole.With(ctx), + service.DefaultAccessControl, + roles, + ) +} diff --git a/system/provision-config.go b/system/provision-config.go new file mode 100644 index 000000000..d78329443 --- /dev/null +++ b/system/provision-config.go @@ -0,0 +1,102 @@ +package system + +import ( + "context" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/titpetric/factory" + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" + impAux "github.com/cortezaproject/corteza-server/pkg/importer" + provision "github.com/cortezaproject/corteza-server/provision/system" + "github.com/cortezaproject/corteza-server/system/importer" + "github.com/cortezaproject/corteza-server/system/repository" + "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" +) + +func provisionConfig(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { + c.Log.Debug("running configuration provision") + c.InitServices(ctx, c) + + // Make sure we have all full access for provisioning + ctx = auth.SetSuperUserContext(ctx) + + if provisioned, err := isProvisioned(ctx); err != nil { + return err + } else if provisioned { + c.Log.Debug("configuration already provisioned") + return nil + } + + readers, err := impAux.ReadStatic(provision.Asset) + if err != nil { + return err + } + + return errors.Wrap( + importer.Import(ctx, readers...), + "could not provision configuration for system service", + ) +} + +// Provision ONLY when there are no roles +func isProvisioned(ctx context.Context) (bool, error) { + rr, err := service.DefaultRole.With(ctx).Find(&types.RoleFilter{}) + return len(rr) > 0, err +} + +func makeDefaultApplications(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { + db, err := factory.Database.Get(system) + if err != nil { + return err + } + + repo := repository.Application(ctx, db) + + aa, err := repo.Find() + if err != nil { + return err + } + + // List of apps to create. + // + // We use Unify.Url field for matching, + // so make sure it's always present! + defApps := types.ApplicationSet{ + &types.Application{ + Name: "CRM", + Enabled: true, + Unify: &types.ApplicationUnify{ + Name: "CRM", + Listed: true, + Icon: "/applications/crust_favicon.png", + Logo: "/applications/crust.jpg", + Url: "/compose/ns/crm/pages", + }, + }, + } + + return defApps.Walk(func(defApp *types.Application) error { + for _, a := range aa { + if a.Unify != nil && a.Unify.Url == defApp.Unify.Url { + // App already added. + return nil + } + } + + defApp, err = repo.Create(defApp) + c.Log.Info( + "creating default application", + zap.String("name", defApp.Name), + zap.Uint64("name", defApp.ID), + zap.Error(err), + ) + return err + + return nil + }) +} diff --git a/system/autosetup.go b/system/provision-oidc-discovery.go similarity index 59% rename from system/autosetup.go rename to system/provision-oidc-discovery.go index bcc76142c..eeb631a15 100644 --- a/system/autosetup.go +++ b/system/provision-oidc-discovery.go @@ -2,86 +2,18 @@ package system import ( "context" - "errors" "strings" + "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/titpetric/factory" "go.uber.org/zap" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/cli/options" "github.com/cortezaproject/corteza-server/system/auth/external" - "github.com/cortezaproject/corteza-server/system/repository" "github.com/cortezaproject/corteza-server/system/service" - "github.com/cortezaproject/corteza-server/system/types" ) -func accessControlSetup(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - c.InitServices(ctx, c) - - // Calling grant directly on internal permissions service to avoid AC check for "grant" - var p = service.DefaultPermissions - var ac = service.DefaultAccessControl - return p.Grant(ctx, ac.Whitelist(), ac.DefaultRules()...) -} - -func makeDefaultApplications(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - db, err := factory.Database.Get(system) - if err != nil { - return err - } - - repo := repository.Application(ctx, db) - - aa, err := repo.Find() - if err != nil { - return err - } - - // List of apps to create. - // - // We use Unify.Url field for matching, - // so make sure it's always present! - defApps := types.ApplicationSet{ - &types.Application{ - Name: "CRM", - Enabled: true, - Unify: &types.ApplicationUnify{ - Name: "CRM", - Listed: true, - Icon: "/applications/crust_favicon.png", - Logo: "/applications/crust.jpg", - Url: "/compose/ns/crm/pages", - }, - }, - } - - return defApps.Walk(func(defApp *types.Application) error { - for _, a := range aa { - if a.Unify != nil && a.Unify.Url == defApp.Unify.Url { - // App already added. - return nil - } - } - - defApp, err = repo.Create(defApp) - c.Log.Info( - "creating default application", - zap.String("name", defApp.Name), - zap.Uint64("name", defApp.ID), - zap.Error(err), - ) - return err - - return nil - }) -} - -func discoverSettings(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - return service.DefaultSettings.With(ctx).AutoDiscovery() -} - func oidcAutoDiscovery(ctx context.Context, cmd *cobra.Command, c *cli.Config) (err error) { var provider = strings.TrimSpace(options.EnvString("", "PROVISION_OIDC_PROVIDER", "")) @@ -117,7 +49,7 @@ func oidcAutoDiscovery(ctx context.Context, cmd *cobra.Command, c *cli.Config) ( if err != nil { c.Log.Error( - "could not register oidc provider", + "could not register OIDC provider", zap.String("url", purl), zap.String("name", name), zap.Error(err)) @@ -126,7 +58,7 @@ func oidcAutoDiscovery(ctx context.Context, cmd *cobra.Command, c *cli.Config) ( c.Log.Info("provider already exists", zap.String("name", name)) } else { - c.Log.Info("provider successfuly registered", + c.Log.Info("provider successfully registered", zap.String("url", purl), zap.String("key", eap.Key), zap.String("name", name)) diff --git a/system/provision-settings.go b/system/provision-settings.go new file mode 100644 index 000000000..fb93e37ff --- /dev/null +++ b/system/provision-settings.go @@ -0,0 +1,14 @@ +package system + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/system/service" +) + +func settingsAutoDiscovery(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { + return service.DefaultSettings.With(ctx).AutoDiscovery() +} diff --git a/system/system.go b/system/system.go index 11a3de94c..4c673cafa 100644 --- a/system/system.go +++ b/system/system.go @@ -57,11 +57,11 @@ func Configure() *cli.Config { c.InitServices(ctx, c) - if c.ProvisionOpt.AutoSetup { - cli.HandleError(accessControlSetup(ctx, cmd, c)) + if c.ProvisionOpt.Configuration { + cli.HandleError(provisionConfig(ctx, cmd, c)) cli.HandleError(makeDefaultApplications(ctx, cmd, c)) - cli.HandleError(discoverSettings(ctx, cmd, c)) + cli.HandleError(settingsAutoDiscovery(ctx, cmd, c)) // Reload auto-configured settings // adding externals and oidc auto discovery depends on redirect-url setting @@ -154,8 +154,8 @@ func Configure() *cli.Config { }, }, - ProvisionAccessControl: cli.Runners{ - accessControlSetup, + ProvisionConfig: cli.Runners{ + provisionConfig, }, } } diff --git a/tests/messaging/provision_test.go b/tests/messaging/provision_test.go index 5418133e5..513ca77c5 100644 --- a/tests/messaging/provision_test.go +++ b/tests/messaging/provision_test.go @@ -20,8 +20,8 @@ func TestProvisioning(t *testing.T) { aux interface{} roles = sysTypes.RoleSet{ - &sysTypes.Role{ID: 1, Handle: "everyone"}, - &sysTypes.Role{ID: 2, Handle: "admins"}, + &sysTypes.Role{ID: permissions.EveryoneRoleID, Handle: "everyone"}, + &sysTypes.Role{ID: permissions.AdminRoleID, Handle: "admins"}, } ctx = h.secCtx() diff --git a/tests/system/provision_test.go b/tests/system/provision_test.go index 36007d951..7fd9ddbf3 100644 --- a/tests/system/provision_test.go +++ b/tests/system/provision_test.go @@ -19,8 +19,8 @@ func TestProvisioning(t *testing.T) { aux interface{} roles = types.RoleSet{ - &types.Role{ID: 1, Handle: "everyone"}, - &types.Role{ID: 2, Handle: "admins"}, + &types.Role{ID: permissions.EveryoneRoleID, Handle: "everyone"}, + &types.Role{ID: permissions.AdminRoleID, Handle: "admins"}, } ctx = h.secCtx()