├── test ├── .gitignore ├── lib │ ├── sample_login_data.js │ └── test_runner.js ├── build │ └── build_app.js ├── time_expense │ └── specs │ │ ├── employeegroup.js │ │ └── employee.js └── routes │ └── routebi │ └── olapdata.js ├── sample └── xtuple-ice-cream │ ├── database │ ├── source │ │ ├── create_ic_schema.sql │ │ ├── priv.sql │ │ ├── manifest.js │ │ ├── icflav.sql │ │ └── cntcticflav.sql │ └── orm │ │ ├── ext │ │ └── contact.json │ │ └── models │ │ └── ice_cream_flavor.json │ ├── client │ ├── views │ │ ├── package.js │ │ ├── list.js │ │ └── workspace.js │ ├── widgets │ │ ├── package.js │ │ ├── picker.js │ │ └── parameter.js │ ├── models │ │ ├── package.js │ │ ├── startup.js │ │ └── ice_cream_flavor.js │ ├── package.js │ ├── core.js │ ├── en │ │ └── strings.js │ └── postbooks.js │ ├── README.md │ ├── package.json │ └── test │ └── ice_cream_flavor.js ├── source ├── bi_open │ ├── database │ │ ├── source │ │ │ ├── public │ │ │ │ └── tables │ │ │ │ │ └── metric.sql │ │ │ ├── create-bi-open-schema.sql │ │ │ ├── manifest.js │ │ │ └── usrbichart.sql │ │ └── orm │ │ │ └── models │ │ │ └── chart.json │ ├── node-datasource │ │ ├── olapcatalog │ │ │ ├── package.json │ │ │ ├── olapsource.js │ │ │ └── olapcatalog.js │ │ └── routes │ │ │ ├── analysis.js │ │ │ └── olapdata.js │ ├── client │ │ ├── views │ │ │ ├── package.js │ │ │ ├── crmdashboard.js │ │ │ └── welcomedashboard.js │ │ ├── models │ │ │ ├── package.js │ │ │ ├── user_bi_chart.js │ │ │ ├── static.js │ │ │ └── analytic.js │ │ ├── package.js │ │ ├── widgets │ │ │ ├── package.js │ │ │ ├── list.js │ │ │ ├── picker.js │ │ │ ├── bi_chart_nopicker.js │ │ │ ├── parameter.js │ │ │ └── bi_chart_measure.js │ │ ├── lib │ │ │ └── package.js │ │ ├── postbooks.js │ │ ├── en │ │ │ └── strings.js │ │ └── core.js │ └── README.md └── time_expense │ ├── client │ ├── root-package.js │ ├── widgets │ │ ├── package.js │ │ ├── picker.js │ │ ├── parameter.js │ │ └── project.js │ ├── package.js │ ├── models │ │ ├── package.js │ │ ├── startup.js │ │ ├── customer.js │ │ ├── item.js │ │ └── project.js │ ├── views │ │ ├── package.js │ │ ├── documents_box.js │ │ ├── list_relations.js │ │ ├── list.js │ │ ├── workspace.js │ │ └── list_relations_editor_box.js │ ├── core.js │ ├── postbooks.js │ └── en │ │ └── strings.js │ └── database │ ├── source │ ├── xt │ │ ├── views │ │ │ ├── prjtaskinfo.sql │ │ │ ├── teexpinfo.sql │ │ │ ├── teprjinfo.sql │ │ │ ├── teprjtaskinfo.sql │ │ │ ├── tecustrateinfo.sql │ │ │ ├── teheadinfo.sql │ │ │ └── teiteminfo.sql │ │ ├── functions │ │ │ ├── te_posted_state.sql │ │ │ ├── te_invoiced_state.sql │ │ │ ├── te_posted_value.sql │ │ │ ├── te_vouchered_state.sql │ │ │ ├── te_total_hours.sql │ │ │ ├── te_total_expenses.sql │ │ │ ├── te_invoiced_value.sql │ │ │ ├── te_vouchered_value.sql │ │ │ ├── te_to_invoice.sql │ │ │ └── te_to_voucher.sql │ │ └── trigger_functions │ │ │ └── teitem_did_change.sql │ ├── te │ │ ├── schema │ │ │ └── create_te_schema.sql │ │ ├── functions │ │ │ ├── unnest.sql │ │ │ ├── copyitem.sql │ │ │ ├── calcrate.sql │ │ │ ├── sheetstate.sql │ │ │ ├── postsheet.sql │ │ │ ├── invoicesheets.sql │ │ │ └── vouchersheet.sql │ │ ├── tables │ │ │ ├── teexp.sql │ │ │ ├── teemp.sql │ │ │ ├── tecustrate.sql │ │ │ ├── teprj.sql │ │ │ ├── teprjtask.sql │ │ │ ├── tehead.sql │ │ │ └── teitem.sql │ │ └── trigger_functions │ │ │ ├── teprj.sql │ │ │ ├── tehead.sql │ │ │ └── teitem.sql │ ├── xm │ │ └── javascript │ │ │ └── project.sql │ ├── priv.sql │ └── manifest.js │ └── orm │ ├── ext │ ├── customer.json │ ├── employee.json │ ├── item.json │ └── project.json │ └── models │ └── project.json ├── .gitignore ├── .gitattributes ├── tools ├── deploy.bat └── deploy.sh ├── README.md ├── .gitmodules ├── .travis.yml ├── package.json └── docs ├── TUTORIAL-FAQ.md └── TUTORIAL4.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | lib/demo-test.backup 2 | lib/login_data.js 3 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/source/create_ic_schema.sql: -------------------------------------------------------------------------------- 1 | select xt.create_schema('ic'); 2 | -------------------------------------------------------------------------------- /source/bi_open/database/source/public/tables/metric.sql: -------------------------------------------------------------------------------- 1 | select setmetric('DashboardLite', 'f'); 2 | -------------------------------------------------------------------------------- /source/time_expense/client/root-package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "source/timeExpense/client" 3 | ); 4 | -------------------------------------------------------------------------------- /source/bi_open/node-datasource/olapcatalog/package.json: -------------------------------------------------------------------------------- 1 | { "name": "olapcatalog", "main": "olapcatalog.js" } -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/views/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "list.js", 3 | "workspace.js" 4 | ); 5 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/widgets/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "picker.js", 3 | "parameter.js" 4 | ); 5 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/models/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "ice_cream_flavor.js", 3 | "startup.js" 4 | ); 5 | -------------------------------------------------------------------------------- /source/time_expense/client/widgets/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "parameter.js", 3 | "picker.js", 4 | "project.js" 5 | ); -------------------------------------------------------------------------------- /source/bi_open/client/views/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "dashboard.js", 3 | "crmdashboard.js", 4 | "welcomedashboard.js" 5 | ); 6 | -------------------------------------------------------------------------------- /source/time_expense/client/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "core.js", 3 | "models", 4 | "widgets", 5 | "views", 6 | "postbooks.js" 7 | ); 8 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "core.js", 3 | "models", 4 | "widgets", 5 | "views", 6 | "postbooks.js" 7 | ); 8 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/source/priv.sql: -------------------------------------------------------------------------------- 1 | select xt.add_priv('MaintainIceCreamFlavors', 'Maintain Ice Cream Flavors', 'IceCream', 'Contact'); 2 | -------------------------------------------------------------------------------- /source/bi_open/client/models/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "analytic.js", 3 | "olap_meta.js", 4 | "static.js", 5 | "user_bi_chart.js" 6 | ); 7 | -------------------------------------------------------------------------------- /source/bi_open/client/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "lib", 3 | "core.js", 4 | "models", 5 | "widgets", 6 | "views", 7 | "postbooks.js" 8 | ); 9 | -------------------------------------------------------------------------------- /source/time_expense/client/models/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "customer.js", 3 | "item.js", 4 | "project.js", 5 | "startup.js", 6 | "worksheet.js" 7 | ); 8 | -------------------------------------------------------------------------------- /source/time_expense/client/views/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "list.js", 3 | "list_relations.js", 4 | "list_relations_editor_box.js", 5 | "documents_box.js", 6 | "workspace.js" 7 | ); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /builds 3 | 4 | # might be left over from failed build 5 | /enyo 6 | /scripts/package.js 7 | /build 8 | /deploy 9 | 10 | 11 | npm-debug.log 12 | *.swp 13 | *.DS_Store 14 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/prjtaskinfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.prjtaskinfo', $$ 2 | 3 | select prjtask.*, prj_number 4 | from prjtask 5 | join prj on prj_id = prjtask_prj_id; 6 | 7 | $$); -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_posted_state.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_posted_state(tehead_id integer) returns boolean stable as $$ 2 | select case te.sheetstate($1, 'P') when 1 then true when 0 then false end; 3 | $$ language sql; 4 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_invoiced_state.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_invoiced_state(tehead_id integer) returns boolean stable as $$ 2 | select case te.sheetstate($1, 'I') when 1 then true when 0 then false end; 3 | $$ language sql; 4 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_posted_value.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_posted_value(tehead_id integer) returns numeric stable as $$ 2 | select sum(teitem_postedvalue) 3 | from te.teitem where teitem_tehead_id=$1 4 | $$ language sql; 5 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_vouchered_state.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_vouchered_state(tehead_id integer) returns boolean stable as $$ 2 | select case te.sheetstate($1, 'V') when 1 then true when 0 then false end; 3 | $$ language sql; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior, change CRLF to LF 2 | * text eol=lf 3 | 4 | # Do not treat these as text 5 | *.jpg binary 6 | *.data binary 7 | *.png binary 8 | *.ttf binary 9 | *.otf binary 10 | *.eot binary 11 | *.gif binary 12 | *.gz binary 13 | *.ico binary 14 | *.phar binary -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_total_hours.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_total_hours(tehead_id integer) returns numeric stable as $$ 2 | select coalesce(sum(case when (teitem_type='T') then teitem_qty else 0 end),0) 3 | from te.teitem 4 | where teitem_tehead_id=$1; 5 | $$ language sql; 6 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/source/manifest.js: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xtuple-ice-cream", 3 | "version": "0.1.3", 4 | "comment": "Ice Cream extension", 5 | "loadOrder": 999, 6 | "dependencies": ["crm"], 7 | "databaseScripts": [ 8 | "create_ic_schema.sql", 9 | "icflav.sql", 10 | "priv.sql", 11 | "cntcticflav.sql" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_total_expenses.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_total_expenses(tehead_id integer) returns numeric stable as $$ 2 | select round(sum(coalesce(currtobase(teitem_curr_id, teitem_total, teitem_workdate),0)),2) 3 | from te.teitem 4 | where teitem_tehead_id=$1 5 | and teitem_type='E'; 6 | $$ language sql; -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_invoiced_value.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_invoiced_value(tehead_id integer) returns numeric stable as $$ 2 | select sum(case when teitem_invcitem_id is not null 3 | then currtobase(teitem_curr_id, teitem_total, current_date) else 0 end) 4 | from te.teitem where teitem_tehead_id=$1 5 | $$ language sql; 6 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_vouchered_value.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_vouchered_value(tehead_id integer) returns numeric stable as $$ 2 | select sum(case when teitem_vodist_id is not null then 3 | currtobase(teitem_curr_id, teitem_total, current_date) else 0 end) 4 | from te.teitem where teitem_tehead_id=$1 5 | $$ language sql; 6 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/README.md: -------------------------------------------------------------------------------- 1 | xtuple-ice-cream 2 | ================ 3 | 4 | Sample xTuple extension, used for our classic tutorial. Help your customers with their 5 | upcoming ice-cream-based marketing campaigns! The walk-through to create this 6 | extension from scratch can be found 7 | [here](https://github.com/xtuple/xtuple-extensions/blob/master/docs/TUTORIAL.md). 8 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/core.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.icecream = {}; 10 | 11 | }()); 12 | -------------------------------------------------------------------------------- /source/time_expense/client/core.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.timeExpense = {}; 10 | 11 | }()); 12 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_to_invoice.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_to_invoice(tehead_id integer) returns numeric stable as $$ 2 | select sum(case when (teitem_billable=true and teitem_invcitem_id is null) then 3 | currtobase(teitem_curr_id, teitem_total, current_date) else 0 end) 4 | from te.teitem where teitem_tehead_id=$1 5 | $$ language sql; 6 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/schema/create_te_schema.sql: -------------------------------------------------------------------------------- 1 | do $$ 2 | /* Only create the schema if it hasn't been created already */ 3 | var res, sql = "select schema_name from information_schema.schemata where schema_name = 'te'", 4 | res = plv8.execute(sql); 5 | if (!res.length) { 6 | sql = "create schema te; grant all on schema xm to group xtrole;" 7 | plv8.execute(sql); 8 | } 9 | $$ language plv8; -------------------------------------------------------------------------------- /source/bi_open/database/source/create-bi-open-schema.sql: -------------------------------------------------------------------------------- 1 | do $$ 2 | /* Only create the schema if it hasn't been created already */ 3 | var res, sql = "select schema_name from information_schema.schemata where schema_name = 'bi_open'", 4 | res = plv8.execute(sql); 5 | if (!res.length) { 6 | sql = "create schema bi_open; grant all on schema bi_open to group xtrole;" 7 | plv8.execute(sql); 8 | } 9 | $$ language plv8; -------------------------------------------------------------------------------- /test/lib/sample_login_data.js: -------------------------------------------------------------------------------- 1 | //----- Data for login function ----- 2 | exports.data = { 3 | webaddress: 'https://localhost:8443', 4 | username: 'admin', //------- Enter the xTuple username 5 | pwd: 'admin', //------ enter the password here 6 | org: 'dev', //------ enter the database name here 7 | suname: '', //-------enter the sauce labs username 8 | sakey: '' //------enter the sauce labs access key 9 | }; 10 | -------------------------------------------------------------------------------- /tools/deploy.bat: -------------------------------------------------------------------------------- 1 | @REM don't watch the sausage being made 2 | @ECHO OFF 3 | 4 | REM the folder this script is in (*/bootplate/tools) 5 | SET TOOLS=%~DP0 6 | 7 | REM enyo location 8 | SET ENYO=%TOOLS%\..\enyo 9 | 10 | REM deploy script location 11 | SET DEPLOY=%ENYO%\tools\deploy.js 12 | 13 | REM node location 14 | SET NODE=node.exe 15 | 16 | REM use node to invoke deploy.js with imported parameters 17 | %NODE% "%DEPLOY%" %* -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/source/icflav.sql: -------------------------------------------------------------------------------- 1 | select xt.create_table('icflav', 'ic'); 2 | 3 | select xt.add_column('icflav','icflav_id', 'serial', 'primary key', 'ic'); 4 | select xt.add_column('icflav','icflav_name', 'text', '', 'ic'); 5 | select xt.add_column('icflav','icflav_description', 'text', '', 'ic'); 6 | select xt.add_column('icflav','icflav_calories', 'integer', '', 'ic'); 7 | 8 | comment on table ic.icflav is 'Ice cream flavors'; 9 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/unnest.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.unnest(anyarray) 2 | RETURNS SETOF anyelement AS 3 | $BODY$ 4 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 5 | -- See www.xtuple.com/CPAL for the full text of the software license. 6 | SELECT $1[i] FROM 7 | generate_series(array_lower($1,1), 8 | array_upper($1,1)) i; 9 | $BODY$ 10 | LANGUAGE 'sql' IMMUTABLE; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Extensions for the xTuple Mobile/Web platform 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/xtuple/xtuple-extensions.png)](https://travis-ci.org/xtuple/xtuple-extensions) 5 | 6 | Thank you for your interest in building an extension for the xTuple web/mobile platform! 7 | 8 | You'll want to follow our [Building an Extension Tutorial](https://github.com/xtuple/xtuple-extensions/blob/master/docs/TUTORIAL.md) 9 | to get started. 10 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/teexp.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | select xt.create_table('teexp', 'te'); 4 | select xt.add_column('teexp','teexp_id', 'integer', 'not null', 'te'); 5 | select xt.add_column('teexp','teexp_expcat_id', 'integer', '', 'te'); 6 | select xt.add_column('teexp','teexp_accnt_id', 'integer', '', 'te'); 7 | select xt.add_primary_key('teexp', 'teexp_id', 'te'); 8 | 9 | comment on table te.teexp is 'Time Expense Item Master'; 10 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "xTuple ", 3 | "name": "xtuple-ice-cream", 4 | "description": "xTuple ice cream extension", 5 | "version": "0.1.3", 6 | "dependencies": { 7 | }, 8 | "peerDependencies": { 9 | "xtuple": "^4.7.0" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/xtuple/xtuple-extensions" 14 | }, 15 | "engines": { 16 | "node": "0.10.x" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/bi_open/database/source/manifest.js: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bi_open", 3 | "version": "", 4 | "comment": "Business Intelligence", 5 | "loadOrder": 50, 6 | "dependencies": [], 7 | "databaseScripts": [ 8 | "create-bi-open-schema.sql", 9 | "usrbichart.sql", 10 | "public/tables/metric.sql" 11 | ], 12 | "routes": [ 13 | { 14 | "path": "queryOlap", 15 | "filename": "routes/olapdata.js", 16 | "functionName": "queryOlapCatalog" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/source/cntcticflav.sql: -------------------------------------------------------------------------------- 1 | select xt.create_table('cntcticflav', 'ic'); 2 | 3 | select xt.add_column('cntcticflav','cntcticflav_id', 'serial', 'primary key', 'ic'); 4 | select xt.add_column('cntcticflav','cntcticflav_cntct_id', 'integer', 'references cntct (cntct_id)', 'ic'); 5 | select xt.add_column('cntcticflav','cntcticflav_icflav_id', 'integer', 'references ic.icflav (icflav_id)', 'ic'); 6 | 7 | comment on table ic.cntcticflav is 'Joins Contact with Ice cream flavor'; 8 | 9 | -------------------------------------------------------------------------------- /source/bi_open/README.md: -------------------------------------------------------------------------------- 1 | xTuple Open BI Extension 2 | ======================== 3 | The xTuple Open BI Extension provides analysis data routes, the dashboard framework 4 | and charts for CRM. The data is provided by the BI Server which can be built and installed with 5 | xTuple Open BI https://github.com/xtuple/bi-open. 6 | 7 | For informaation on building and installing Open BI and the Open BI Extension see: 8 | https://github.com/xtuple/xtuple/wiki/Installing-Open-Business-Intelligence-using-Vagrant 9 | -------------------------------------------------------------------------------- /source/bi_open/client/widgets/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "bi_chart.js", 3 | "bi_chart_dim_measure.js", 4 | "bi_chart_type_measure.js", 5 | "bi_chart_measure.js", 6 | "bi_chart_nopicker.js", 7 | "bi_comparetimesum_chart.js", 8 | "bi_funnel_chart.js", 9 | "bi_map_chart.js", 10 | "bi_timeseries_chart.js", 11 | "bi_toplist_chart.js", 12 | "crm_funnel_chart.js", 13 | "crm_timeseries_chart.js", 14 | "crm_toplist_chart.js", 15 | "list.js", 16 | "parameter.js", 17 | "picker.js" 18 | ); -------------------------------------------------------------------------------- /tools/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # the folder this script is in (*/bootplate/tools) 4 | TOOLS=$(cd `dirname $0` && pwd) 5 | 6 | # enyo location 7 | ENYO="$TOOLS/../enyo" 8 | 9 | # deploy script location 10 | DEPLOY="$ENYO/tools/deploy.js" 11 | 12 | # check for node, but quietly 13 | if command -v node >/dev/null 2>&1; then 14 | # use node to invoke deploy with imported parameters 15 | echo "enyo/tools/minify.sh args: " $@ 16 | node $DEPLOY $@ 17 | else 18 | echo "No node found in path" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/widgets/picker.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, trailing:true, white:true */ 3 | /*global XT:true, XM:true, enyo:true, _:true */ 4 | 5 | (function () { 6 | 7 | XT.extensions.icecream.initPicker = function () { 8 | 9 | enyo.kind({ 10 | name: "XV.IceCreamFlavorPicker", 11 | kind: "XV.PickerWidget", 12 | collection: "XM.iceCreamFlavors" 13 | }); 14 | }; 15 | 16 | }()); 17 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/teemp.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | select xt.create_table('teemp', 'te'); 4 | select xt.add_column('teemp','teemp_id', 'serial', 'not null', 'te'); 5 | select xt.add_column('teemp','teemp_emp_id', 'integer', '', 'te'); 6 | select xt.add_column('teemp','teemp_contractor', 'boolean', 'default false', 'te'); 7 | select xt.add_constraint('teemp', 'teemp_teemp_emp_id_fkey', 'foreign key (teemp_emp_id) references emp (emp_id)', 'te'); 8 | 9 | comment on table te.teemp is 'Time Expense Employee'; 10 | -------------------------------------------------------------------------------- /source/time_expense/client/views/documents_box.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XV:true, XM:true, _:true, Backbone:true, enyo:true, XT:true */ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initDocumentBox = function () { 9 | enyo.kind({ 10 | name: "XV.WorksheetDocumentsBox", 11 | kind: "XV.DocumentsBox", 12 | parentKey: "worksheet" 13 | }); 14 | }; 15 | 16 | }()); 17 | -------------------------------------------------------------------------------- /source/bi_open/client/lib/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "dimple/lib/d3.v3.4.8.min.js", 3 | "dimple/dist/dimple.v2.0.2.min.js", 4 | "RGraph/libraries/RGraph.common.core.js", 5 | "RGraph/libraries/RGraph.funnel.js", 6 | "RGraph/libraries/RGraph.common.tooltips.js", 7 | "RGraph/libraries/RGraph.common.dynamic.js" 8 | //"leaflet/dist/leaflet73.css", 9 | //"leaflet/dist/leaflet73-src.js", 10 | //"leaflet-markercluster/dist/leaflet.markercluster-src.js", 11 | //"leaflet-markercluster/dist/MarkerCluster.css", 12 | //"leaflet-markercluster/dist/MarkerCluster.Default.css" 13 | ); 14 | -------------------------------------------------------------------------------- /source/time_expense/client/models/startup.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.timeExpense.initStartup = function () { 10 | XT.cacheCollection("XM.expenseCategories", "XM.ExpenseCategoryCollection", "code"); 11 | XT.cacheCollection("XM.siteRelations", "XM.SiteRelationCollection", "code"); 12 | }; 13 | 14 | }()); 15 | -------------------------------------------------------------------------------- /source/time_expense/client/widgets/picker.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, trailing:true, white:true */ 3 | /*global XT:true, XM:true, enyo:true, _:true */ 4 | 5 | (function () { 6 | 7 | // .......................................................... 8 | // ITEM EXPENSE OPTIONS 9 | // 10 | 11 | enyo.kind({ 12 | name: "XV.ItemExpenseOptionsPicker", 13 | kind: "XV.PickerWidget", 14 | collection: "XM.itemExpenseOptions", 15 | noneText: "_notUsed".loc() 16 | }); 17 | 18 | }()); 19 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/models/startup.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.icecream.initStartup = function () { 10 | XT.cacheCollection("XM.iceCreamFlavors", "XM.IceCreamFlavorCollection"); 11 | 12 | XT.Error.addError({ 13 | code: "icecream3001", 14 | messageKey: "_mustUseLite" 15 | }); 16 | }; 17 | 18 | }()); 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | 2 | [submodule "source/bi_open/client/lib/leaflet"] 3 | path = source/bi_open/client/lib/leaflet 4 | url = https://github.com/Leaflet/Leaflet.git 5 | [submodule "source/bi_open/client/lib/leaflet-markercluster"] 6 | path = source/bi_open/client/lib/leaflet-markercluster 7 | url = https://github.com/Leaflet/Leaflet.markercluster.git 8 | [submodule "source/bi_open/client/lib/dimple"] 9 | path = source/bi_open/client/lib/dimple 10 | url = https://github.com/PMSI-AlignAlytics/dimple.git 11 | [submodule "source/bi_open/client/lib/RGraph"] 12 | path = source/bi_open/client/lib/RGraph 13 | url = https://github.com/xtuple/RGraph.git 14 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/copyitem.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.copyItem(INTEGER, TEXT) RETURNS INTEGER AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | pSItemid ALIAS FOR $1; 6 | pTItemNumber ALIAS FOR $2; 7 | _itemid INTEGER; 8 | _r RECORD; 9 | _id INTEGER; 10 | 11 | BEGIN 12 | _itemid := public.copyItem(pSItemid, pTItemNumber); 13 | 14 | INSERT INTO te.teexp 15 | SELECT _itemid, teexp_expcat_id, teexp_accnt_id 16 | FROM te.teexp src 17 | WHERE (src.teexp_id=pSItemid); 18 | 19 | RETURN _itemid; 20 | END; 21 | $$ LANGUAGE 'plpgsql'; 22 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/widgets/parameter.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, trailing:true, white:true */ 3 | /*global XT:true, XV:true, enyo:true, _:true */ 4 | 5 | (function () { 6 | 7 | XT.extensions.icecream.initParameterWidget = function () { 8 | 9 | var extensions = [ 10 | {kind: "onyx.GroupboxHeader", content: "_iceCreamFlavor".loc()}, 11 | {name: "iceCreamFlavor", label: "_favoriteFlavor".loc(), 12 | attr: "favoriteFlavor", defaultKind: "XV.IceCreamFlavorPicker"} 13 | ]; 14 | 15 | XV.appendExtension("XV.ContactListParameters", extensions); 16 | }; 17 | 18 | }()); 19 | -------------------------------------------------------------------------------- /source/bi_open/client/views/crmdashboard.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, nv:true, d3:true, console:true */ 5 | 6 | (function () { 7 | 8 | enyo.kind({ 9 | name: "XV.CrmBiDashboard", 10 | kind: "XV.BiDashboardCharts", 11 | collection: "XM.UserBiChartCollection", 12 | // title is what show in the "add chart" picker on the 13 | // dashboard and the chart is the widget to be added 14 | // this tells the default query what extension to pull charts for 15 | extension: "crm" 16 | }); 17 | }()); 18 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/functions/te_to_voucher.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.te_to_voucher(tehead_id integer) returns numeric stable as $$ 2 | select sum(case when (teitem_type='E' and teitem_prepaid=false and teitem_vodist_id is null) then teitem_total 3 | when (teitem_type='T' and teemp_contractor=true and teitem_vodist_id is null) then 4 | coalesce(teitem_empcost, te.calcRate(emp_wage, emp_wage_period)) * teitem_qty 5 | else 0 end) 6 | from te.tehead 7 | join emp on (tehead_emp_id=emp_id) 8 | left outer join te.teitem on (tehead_id=teitem_tehead_id) 9 | left outer join te.teemp on (tehead_emp_id=teemp_emp_id) 10 | where tehead_id=$1 11 | $$ language sql; 12 | -------------------------------------------------------------------------------- /source/bi_open/client/views/welcomedashboard.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, nv:true, d3:true, console:true */ 5 | 6 | (function () { 7 | 8 | enyo.kind({ 9 | name: "XV.WelcomeDashboard", 10 | kind: "XV.BiDashboardCharts", 11 | collection: "XM.UserBiChartCollection", 12 | // title is what show in the "add chart" picker on the 13 | // dashboard and the chart is the widget to be added 14 | // this tells the default query what extension to pull charts for 15 | extension: "welcome" 16 | }); 17 | 18 | }()); 19 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/en/strings.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | strict:true, trailing:true, white:true */ 4 | /*global XT:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | var lang = XT.stringsFor("en_US", { 10 | "_calories": "Calories", 11 | "_favoriteFlavor": "Favorite Flavor", 12 | "_iceCream": "Ice Cream", 13 | "_iceCreamFlavor": "Ice Cream Flavor", 14 | "_iceCreamFlavors": "Ice Cream Flavors", 15 | "_maintainIceCreamFlavors": "Maintain Ice Cream Flavors", 16 | "_mustUseLite": "Any flavor under 450 calories must start with the word 'Lite'" 17 | }); 18 | 19 | if (typeof exports !== 'undefined') { 20 | exports.language = lang; 21 | } 22 | }()); 23 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/trigger_functions/teprj.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.triggerteprj() RETURNS "trigger" AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | _update BOOLEAN := false; 6 | BEGIN 7 | 8 | IF (TG_OP = 'INSERT') THEN 9 | _update = true; 10 | ELSIF (TG_OP = 'UPDATE') THEN 11 | IF (COALESCE(OLD.teprj_cust_id,-1) != COALESCE(NEW.teprj_cust_id,-1)) THEN 12 | _update = true; 13 | END IF; 14 | END IF; 15 | 16 | IF (_update) THEN 17 | UPDATE te.teprjtask SET teprjtask_cust_id=NEW.teprj_cust_id 18 | FROM prjtask 19 | WHERE ((teprjtask_prjtask_id=prjtask_id) 20 | AND (prjtask_prj_id=NEW.teprj_prj_id)); 21 | END IF; 22 | 23 | RETURN NEW; 24 | END; 25 | $$ LANGUAGE 'plpgsql'; 26 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/postbooks.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, enyo:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.icecream.initPostbooks = function () { 10 | var panels, relevantPrivileges; 11 | 12 | panels = [ 13 | {name: "iceCreamFlavorList", kind: "XV.IceCreamFlavorList"} 14 | ]; 15 | XT.app.$.postbooks.appendPanels("setup", panels); 16 | 17 | relevantPrivileges = [ 18 | "MaintainIceCreamFlavors" 19 | ]; 20 | XT.session.addRelevantPrivileges("xtuple-ice-cream", relevantPrivileges); 21 | XT.session.privilegeSegments.Contact.push("MaintainIceCreamFlavors"); 22 | 23 | }; 24 | }()); 25 | 26 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/tecustrate.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | select xt.create_table('tecustrate', 'te'); 4 | select xt.add_column('tecustrate','tecustrate_id', 'serial', 'not null', 'te'); 5 | select xt.add_column('tecustrate','tecustrate_cust_id', 'integer', 'not null', 'te'); 6 | select xt.add_column('tecustrate','tecustrate_rate', 'numeric(16,4)', 'not null', 'te'); 7 | select xt.add_column('tecustrate','tecustrate_curr_id', 'integer', 'not null default basecurrid()', 'te'); 8 | select xt.add_primary_key('tecustrate', 'tecustrate_cust_id', 'te'); 9 | select xt.add_constraint('tecustrate', 'tecustrate_tecustrate_cust_id_fkey', 'foreign key (tecustrate_cust_id) references custinfo (cust_id)', 'te'); 10 | select xt.add_constraint('tecustrate', 'tecustrate_tecustrate_curr_id_fkey', 'foreign key (tecustrate_curr_id) references curr_symbol (curr_id)', 'te'); 11 | 12 | comment on table te.tecustrate is 'Customer rate'; 13 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/teexpinfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.teexpinfo', $$ 2 | select teexp.*, 3 | case when teexp_expcat_id is not null then 'E' 4 | when teexp_accnt_id is not null then 'A' 5 | end as teexp_method 6 | from te.teexp 7 | $$, false); 8 | 9 | create or replace rule "_INSERT" as on insert to xt.teexpinfo do instead 10 | 11 | insert into te.teexp ( 12 | teexp_id, 13 | teexp_expcat_id, 14 | teexp_accnt_id 15 | ) values ( 16 | new.teexp_id, 17 | new.teexp_expcat_id, 18 | new.teexp_accnt_id 19 | ); 20 | 21 | create or replace rule "_UPDATE" as on update to xt.teexpinfo do instead 22 | 23 | update te.teexp set 24 | teexp_expcat_id = new.teexp_expcat_id, 25 | teexp_accnt_id = new.teexp_accnt_id 26 | where teexp_id = old.teexp_id; 27 | 28 | create or replace rule "_DELETE" as on delete to xt.teexpinfo do instead 29 | 30 | delete from te.teexp where teexp_id = old.teexp_id; 31 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xm/javascript/project.sql: -------------------------------------------------------------------------------- 1 | select xt.install_js('XM','Project','xtte', $$ 2 | /* Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | See www.xm.ple.com/CPAL for the full text of the software license. */ 4 | 5 | (function () { 6 | 7 | if (!XM.Project) { XM.Project = {}; } 8 | 9 | XM.Project.isDispatchable = true; 10 | 11 | /** 12 | Determine if a project is referenced by worksheets 13 | 14 | @param {String} Project number 15 | @returns Boolean 16 | */ 17 | XM.Project.worksheetUsed = function(projectNumber) { 18 | var sql = "select count(teitem_id) as used " + 19 | "from prj " + 20 | " join prjtask on prj_id=prjtask_prj_id " + 21 | " join te.teitem on teitem_prjtask_id=prjtask_id " + 22 | "where prj_number = $1;"; 23 | return plv8.execute(sql, [projectNumber])[0].used; 24 | }; 25 | 26 | }()); 27 | 28 | $$); 29 | 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10.32" 4 | 5 | install: 6 | - "cd .." 7 | - "git clone git://github.com/xtuple/xtuple.git --recursive" 8 | - "cd xtuple" 9 | - "bash scripts/install_xtuple.sh -ipn" 10 | - "cp test/lib/login_data.js ../xtuple-extensions/test/lib/login_data.js" 11 | - "cd ../xtuple-extensions" 12 | - "npm install" 13 | - "npm run-script test-build" 14 | - "mv sample/xtuple-ice-cream source" 15 | - "mkdir -p test/xtuple-ice-cream/specs" 16 | - "cp source/xtuple-ice-cream/test/* test/xtuple-ice-cream/specs" 17 | - "../xtuple/scripts/build_app.js -e source/xtuple-ice-cream" 18 | 19 | before_script: 20 | - "cd ../xtuple/node-datasource" 21 | - "node main.js &" 22 | - "sleep 10" 23 | - "cd .." 24 | 25 | script: 26 | - "npm test" 27 | - "npm run-script test-datasource" 28 | - "cd ../xtuple-extensions" 29 | - "npm test" 30 | - "../xtuple/node_modules/.bin/jshint --exclude source/bi_open/client/lib source" 31 | -------------------------------------------------------------------------------- /source/time_expense/database/orm/ext/customer.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "time_expense", 4 | "nameSpace": "XM", 5 | "type": "Customer", 6 | "table": "xt.tecustrateinfo", 7 | "isExtension": true, 8 | "comment": "Extended by Time Expense", 9 | "relations": [ 10 | { 11 | "column": "tecustrate_cust_id", 12 | "inverse": "id" 13 | } 14 | ], 15 | "properties": [ 16 | { 17 | "name": "isSpecifiedRate", 18 | "attr": { 19 | "type": "Boolean", 20 | "column": "tecustrate_specified_rate" 21 | } 22 | }, 23 | { 24 | "name": "billingRate", 25 | "attr": { 26 | "type": "Number", 27 | "column": "tecustrate_rate" 28 | } 29 | }, 30 | { 31 | "name": "billingCurrency", 32 | "toOne": { 33 | "type": "Currency", 34 | "column": "tecustrate_curr_id" 35 | } 36 | } 37 | ], 38 | "isSystem": true 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/calcrate.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.calcrate(numeric, char(2)) RETURNS numeric AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | pAmount ALIAS FOR $1; 6 | pPeriod ALIAS FOR $2; 7 | _state integer; 8 | _count integer; 9 | 10 | BEGIN 11 | -- Convert amount to hourly rate 12 | IF (pPeriod = 'H') THEN -- hourly 13 | RETURN round(pAmount,2); 14 | ELSIF (pPeriod = 'D') THEN -- daily 15 | RETURN round(pAmount / 8, 2); 16 | ELSIF (pPeriod = 'W') THEN -- weekly 17 | RETURN round(pAmount / 40, 2); 18 | ELSIF (pPeriod = 'BW') THEN -- bi-weekly 19 | RETURN round(pAmount / 80, 2); 20 | ELSIF (pPeriod = 'M') THEN -- monthly 21 | RETURN round(pAmount / 160, 2); 22 | ELSIF (pPeriod = 'Y') THEN -- annually 23 | RETURN round(pAmount / 2080, 2); 24 | ELSE 25 | RAISE EXCEPTION 'Unknown period type passed: %', pPeriod; 26 | END IF; 27 | 28 | END; 29 | $$ LANGUAGE 'plpgsql'; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "author": "xTuple ", 4 | "name": "xtuple-extensions", 5 | "description": "xTuple Enterprise Resource Planning Mobile-Web client public extensions", 6 | "version": "4.8.0-beta", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/xtuple/xtuple-extensions.git" 10 | }, 11 | "dependencies": { 12 | "async":"0.2.x", 13 | "rimraf":"2.1.x", 14 | "underscore":"1.4.x", 15 | "xmla4js": "git://github.com/rpbouman/xmla4js.git" 16 | }, 17 | "devDependencies": { 18 | "chai":"1.5.x", 19 | "html5": "0.3.13", 20 | "mocha":"1.9.x", 21 | "zombie":"1.4.x" 22 | }, 23 | "optionalDependencies": {}, 24 | "engines": { 25 | "node": "0.10.32" 26 | }, 27 | "scripts": { 28 | "test-build": "./node_modules/.bin/mocha -R spec test/build/build_app.js", 29 | "test": "./node_modules/.bin/mocha -R spec test/lib/test_runner.js", 30 | "test-bi_open": "cd ../xtuple/node-datasource; ../node_modules/.bin/mocha -t 40000 -R spec ../../xtuple-extensions/test/routes/routebi/*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/bi_open/client/models/user_bi_chart.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | /** 10 | @class 11 | 12 | @extends XM.Model 13 | */ 14 | XM.UserBiChart = XM.Model.extend({ 15 | /** @scope XM.UserBiChart.prototype */ 16 | 17 | recordType: 'XM.UserBiChart', 18 | 19 | defaults: function () { 20 | return { 21 | username: XM.currentUser.get("username"), 22 | filter: "all" 23 | }; 24 | } 25 | 26 | }); 27 | 28 | /** 29 | @class 30 | 31 | @extends XM.Collection 32 | */ 33 | XM.UserBiChartCollection = XM.Collection.extend({ 34 | /** @scope XM.UserBiChartCollection.prototype */ 35 | 36 | model: XM.UserBiChart, 37 | 38 | orderAttribute: { 39 | orderBy: [{ 40 | attribute: "order" 41 | }] 42 | } 43 | 44 | }); 45 | 46 | }()); 47 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/teprjinfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.teprjinfo', $$ 2 | select teprj.*, 3 | case when teprj_curr_id is not null then true 4 | else false 5 | end as teprj_specified_rate 6 | from te.teprj 7 | $$, false); 8 | 9 | create or replace rule "_INSERT" as on insert to xt.teprjinfo do instead 10 | 11 | insert into te.teprj ( 12 | teprj_id, 13 | teprj_cust_id, 14 | teprj_prj_id, 15 | teprj_rate, 16 | teprj_curr_id 17 | ) values ( 18 | coalesce(new.teprj_id, nextval('te.teprj_teprj_id_seq')), 19 | new.teprj_cust_id, 20 | new.teprj_prj_id, 21 | new.teprj_rate, 22 | new.teprj_curr_id 23 | ); 24 | 25 | create or replace rule "_UPDATE" as on update to xt.teprjinfo do instead 26 | 27 | update te.teprj set 28 | teprj_cust_id=new.teprj_cust_id, 29 | teprj_prj_id=new.teprj_prj_id, 30 | teprj_rate=new.teprj_rate, 31 | teprj_curr_id=new.teprj_curr_id 32 | where teprj_id = old.teprj_id; 33 | 34 | create or replace rule "_DELETE" as on delete to xt.teprjinfo do instead 35 | 36 | delete from te.teprj where teprj_id = old.teprj_id; 37 | -------------------------------------------------------------------------------- /source/bi_open/database/source/usrbichart.sql: -------------------------------------------------------------------------------- 1 | select xt.create_table('usrbichart', 'bi_open'); 2 | 3 | select xt.add_column('usrbichart','usrbichart_id', 'serial', 'primary key', 'bi_open'); 4 | select xt.add_column('usrbichart','usrbichart_usr_username', 'text', '', 'bi_open'); 5 | select xt.add_column('usrbichart','usrbichart_chart', 'text', '', 'bi_open'); 6 | select xt.add_column('usrbichart','usrbichart_ext_name', 'text', '', 'bi_open'); 7 | select xt.add_column('usrbichart','usrbichart_filter_option', 'text', '', 'bi_open'); 8 | select xt.add_column('usrbichart','usrbichart_groupby_option', 'text', '', 'bi_open'); 9 | select xt.add_column('usrbichart','usrbichart_measure', 'text', '', 'bi_open'); 10 | select xt.add_column('usrbichart','usrbichart_charttype', 'text', '', 'bi_open'); 11 | select xt.add_column('usrbichart','usrbichart_dimension', 'text', '', 'bi_open'); 12 | select xt.add_column('usrbichart','usrbichart_order', 'integer', '', 'bi_open'); 13 | select xt.add_column('usrbichart','usrbichart_uuid_filter', 'text', '', 'bi_open'); 14 | 15 | comment on table bi_open.usrbichart is 'Charts users have selected for dashboard'; 16 | -------------------------------------------------------------------------------- /source/time_expense/database/source/priv.sql: -------------------------------------------------------------------------------- 1 | -- add necessary privs 2 | 3 | select xt.add_priv('MaintainTimeExpenseOthers', 'Allowed to Maintain Time/Exp Sheets for all users', 'TE', 'TE'); 4 | select xt.add_priv('MaintainTimeExpenseSelf', 'Allowed to Maintain Time/Exp Sheets', 'TE', 'TE'); 5 | select xt.add_priv('MaintainTimeExpense', 'Allowed to Maintain Time/Exp Sheets', 'TE', 'TE'); 6 | select xt.add_priv('CanViewRates', 'Allowed to view rates in the Time Entries', 'TE', 'TE'); 7 | select xt.add_priv('MaintainEmpCostAll', 'Allowed to maintain employee costs for all users', 'TE', 'TE'); 8 | select xt.add_priv('MaintainEmpCostSelf', 'Allowed to maintain own employee costs', 'TE', 'TE'); 9 | select xt.add_priv('CanApprove', 'Allowed to Approve Time/Exp Sheets', 'TE', 'TE'); 10 | select xt.add_priv('allowInvoicing', 'Allowed to Invoice Time/Exp Sheets', 'TE', 'TE'); 11 | select xt.add_priv('allowVouchering', 'Allowed to Voucher Time/Exp Sheets', 'TE', 'TE'); 12 | select xt.add_priv('PostTimeSheets', 'Allowed to Post Time Sheets', 'TE', 'TE'); 13 | select xt.add_priv('ViewTimeExpenseHistory', 'Allowed to view Time Expense Sheet history', 'TE', 'TE'); 14 | 15 | 16 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/teprj.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | select xt.create_table('teprj', 'te'); 4 | 5 | -- remove old trigger if any 6 | drop trigger if exists teprjtrigger on te.teprj; 7 | 8 | select xt.add_column('teprj','teprj_id', 'serial', 'not null', 'te'); 9 | select xt.add_column('teprj','teprj_prj_id', 'integer', '', 'te'); 10 | select xt.add_column('teprj','teprj_cust_id', 'integer', '', 'te'); 11 | select xt.add_column('teprj','teprj_rate', 'numeric', '', 'te'); 12 | select xt.add_column('teprj','teprj_curr_id', 'integer', '', 'te'); 13 | select xt.add_primary_key('teprj', 'teprj_id', 'te'); 14 | select xt.add_constraint('teprj', 'teprj_teprj_curr_id_fkey','foreign key (teprj_curr_id) references curr_symbol (curr_id)', 'te'); 15 | select xt.add_constraint('teprj', 'teprj_teprj_cust_id_fkey','foreign key (teprj_cust_id) references custinfo (cust_id)', 'te'); 16 | select xt.add_constraint('teprj', 'teprj_teprj_prj_id','unique(teprj_prj_id)', 'te'); 17 | comment on table te.teprj is 'Time Expense Project'; 18 | 19 | -- create trigger 20 | 21 | create trigger teprjtrigger after insert or update on te.teprj for each row execute procedure te.triggerteprj(); -------------------------------------------------------------------------------- /source/time_expense/database/orm/ext/employee.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "time_expense", 4 | "nameSpace": "XM", 5 | "type": "Employee", 6 | "table": "te.teemp", 7 | "isExtension": true, 8 | "comment": "Extended by Time Expense", 9 | "relations": [ 10 | { 11 | "column": "teemp_emp_id", 12 | "inverse": "id" 13 | } 14 | ], 15 | "properties": [ 16 | { 17 | "name": "isContractor", 18 | "attr": { 19 | "type": "Boolean", 20 | "column": "teemp_contractor" 21 | } 22 | } 23 | ], 24 | "isSystem": true 25 | }, 26 | { 27 | "context": "time_expense", 28 | "nameSpace": "XM", 29 | "type": "EmployeeRelation", 30 | "table": "te.teemp", 31 | "isExtension": true, 32 | "comment": "Extended by Time Expense", 33 | "relations": [ 34 | { 35 | "column": "teemp_emp_id", 36 | "inverse": "id" 37 | } 38 | ], 39 | "properties": [ 40 | { 41 | "name": "isContractor", 42 | "attr": { 43 | "type": "Boolean", 44 | "column": "teemp_contractor" 45 | } 46 | } 47 | ], 48 | "isSystem": true 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /source/bi_open/node-datasource/olapcatalog/olapsource.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, console:true, issue:true, require:true, XM:true, io:true, 5 | Backbone:true, _:true, X:true, __dirname:true, exports:true */ 6 | 7 | (function () { 8 | "use strict"; 9 | 10 | exports.olapSource = X.olapSource = X.olapCatalog.create({ 11 | 12 | /** 13 | * Initializes olapSource 14 | */ 15 | init: function () { 16 | }, 17 | 18 | /** 19 | Perform xmla query appending jwt for single sign on 20 | 21 | @param {String} query 22 | @param {String} jwt 23 | @param {Function} callback 24 | */ 25 | query: function (query, jwt, callback) { 26 | this.xmlaConnect.executeTabular({ 27 | statement: query, 28 | url : "http://" + this.hostname + ":" + this.port + "/pentaho/Xmla?assertion=" + jwt.jwt, 29 | success: function (xmla, options, xmlaResponse) { 30 | callback(xmlaResponse); // back to callback in olapdata 31 | }, 32 | }); 33 | } 34 | 35 | }); 36 | 37 | }()); 38 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/views/list.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, enyo:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.icecream.initList = function () { 10 | enyo.kind({ 11 | name: "XV.IceCreamFlavorList", 12 | kind: "XV.List", 13 | label: "_iceCreamFlavors".loc(), 14 | collection: "XM.IceCreamFlavorCollection", 15 | query: {orderBy: [ 16 | {attribute: 'name'} 17 | ]}, 18 | components: [ 19 | {kind: "XV.ListItem", components: [ 20 | {kind: "FittableColumns", components: [ 21 | {kind: "XV.ListColumn", classes: "medium", 22 | components: [ 23 | {kind: "XV.ListAttr", attr: "name", isKey: true}, 24 | {kind: "XV.ListAttr", attr: "description"} 25 | ]}, 26 | {kind: "XV.ListColumn", fit: true, components: [ 27 | {kind: "XV.ListAttr", attr: "calories"} 28 | ]} 29 | ]} 30 | ]} 31 | ] 32 | }); 33 | }; 34 | }()); 35 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/orm/ext/contact.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "xtuple-ice-cream", 4 | "nameSpace": "XM", 5 | "type": "Contact", 6 | "table": "ic.cntcticflav", 7 | "isExtension": true, 8 | "isChild": false, 9 | "comment": "Extended by Icecream", 10 | "relations": [ 11 | { 12 | "column": "cntcticflav_cntct_id", 13 | "inverse": "id" 14 | } 15 | ], 16 | "properties": [ 17 | { 18 | "name": "favoriteFlavor", 19 | "toOne": { 20 | "type": "IceCreamFlavor", 21 | "column": "cntcticflav_icflav_id" 22 | } 23 | } 24 | ], 25 | "isSystem": true 26 | }, 27 | { 28 | "context": "xtuple-ice-cream", 29 | "nameSpace": "XM", 30 | "type": "ContactListItem", 31 | "table": "ic.cntcticflav", 32 | "isExtension": true, 33 | "isChild": false, 34 | "comment": "Extended by Icecream", 35 | "relations": [ 36 | { 37 | "column": "cntcticflav_cntct_id", 38 | "inverse": "id" 39 | } 40 | ], 41 | "properties": [ 42 | { 43 | "name": "favoriteFlavor", 44 | "toOne": { 45 | "type": "IceCreamFlavor", 46 | "column": "cntcticflav_icflav_id" 47 | } 48 | } 49 | ], 50 | "isSystem": true 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /source/bi_open/client/widgets/list.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true, strict: false*/ 4 | /*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, Globalize:true*/ 5 | 6 | (function () { 7 | 8 | /** 9 | An abstract list to be used for email profiles 10 | */ 11 | enyo.kind({ 12 | name: "XV.Toplist", 13 | kind: "List", 14 | count: 3, 15 | rowsPerPage: 7, 16 | published: { 17 | data: [] 18 | }, 19 | onSetupItem: "setupItem", 20 | components: [ 21 | {kind: "XV.ListItem", name: "theitems", classes: "xv-list-item", components: [ 22 | {kind: "FittableColumns", components: [ 23 | {kind: "XV.ListColumn", classes: "short", components: [ 24 | {kind: "XV.ListAttr", name: "code", ontap: "clickDrill", classes: "hyperlink bold" }, 25 | ]}, 26 | {kind: "XV.ListColumn", classes: "long", components: [ 27 | {kind: "XV.ListAttr", name: "name"}, 28 | ]}, 29 | {kind: "XV.ListColumn", classes: "medium", components: [ 30 | {kind: "XV.ListAttr", name: "measure", classes: "right"} 31 | ]}, 32 | ]} 33 | ]} 34 | ] 35 | }); 36 | }()); 37 | -------------------------------------------------------------------------------- /source/bi_open/client/widgets/picker.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, trailing:true, white:true, strict:false */ 3 | /*global XT:true, XM:true, enyo:true, _:true */ 4 | 5 | (function () { 6 | 7 | // .......................................................... 8 | // END YEAR 9 | 10 | enyo.kind({ 11 | name: "XV.EndYearPicker", 12 | kind: "XV.PickerWidget", 13 | showNone: false, 14 | collection: "XM.endYears", 15 | valueAttribute: "id", 16 | defaultValue: "_current".loc(), 17 | noneTextChanged: function () { 18 | this.setNoneText("_current".loc()); 19 | this.inherited(arguments); 20 | }, 21 | //create: function () { 22 | // this.inherited(arguments); 23 | // this.setNoneText("_current".loc()); 24 | //} 25 | }); 26 | 27 | // .......................................................... 28 | // END MONTH 29 | 30 | enyo.kind({ 31 | name: "XV.EndMonthPicker", 32 | kind: "XV.PickerWidget", 33 | showNone: false, 34 | collection: "XM.endMonths", 35 | valueAttribute: "id", 36 | defaultValue: "_current".loc(), 37 | noneTextChanged: function () { 38 | this.setNoneText("_current".loc()); 39 | this.inherited(arguments); 40 | }, 41 | }); 42 | 43 | }()); 44 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/teprjtaskinfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.teprjtaskinfo', $$ 2 | select teprjtask.*, 3 | case when teprjtask_curr_id is not null then true 4 | else false 5 | end as teprjtask_specified_rate 6 | from te.teprjtask 7 | $$, false); 8 | 9 | create or replace rule "_INSERT" as on insert to xt.teprjtaskinfo do instead 10 | 11 | insert into te.teprjtask ( 12 | teprjtask_id, 13 | teprjtask_cust_id, 14 | teprjtask_item_id, 15 | teprjtask_prjtask_id, 16 | teprjtask_rate, 17 | teprjtask_curr_id 18 | ) values ( 19 | coalesce(new.teprjtask_id, nextval('te.teprjtask_teprjtask_id_seq')), 20 | new.teprjtask_cust_id, 21 | new.teprjtask_item_id, 22 | new.teprjtask_prjtask_id, 23 | new.teprjtask_rate, 24 | new.teprjtask_curr_id 25 | ); 26 | 27 | create or replace rule "_UPDATE" as on update to xt.teprjtaskinfo do instead 28 | 29 | update te.teprjtask set 30 | teprjtask_cust_id=new.teprjtask_cust_id, 31 | teprjtask_item_id=new.teprjtask_item_id, 32 | teprjtask_prjtask_id=new.teprjtask_prjtask_id, 33 | teprjtask_rate=new.teprjtask_rate, 34 | teprjtask_curr_id=new.teprjtask_curr_id 35 | where teprjtask_id = old.teprjtask_id; 36 | 37 | create or replace rule "_DELETE" as on delete to xt.teprjtaskinfo do instead 38 | 39 | delete from te.teprjtask where teprjtask_id = old.teprjtask_id; 40 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/database/orm/models/ice_cream_flavor.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "xtuple-ice-cream", 4 | "nameSpace": "XM", 5 | "type": "IceCreamFlavor", 6 | "table": "ic.icflav", 7 | "idSequenceName": "ic.icflav_icflav_id_seq", 8 | "lockable": true, 9 | "comment": "Ice Cream Flavor Map", 10 | "privileges": { 11 | "all": { 12 | "create": "MaintainIceCreamFlavors", 13 | "read": true, 14 | "update": "MaintainIceCreamFlavors", 15 | "delete": "MaintainIceCreamFlavors" 16 | } 17 | }, 18 | "properties": [ 19 | { 20 | "name": "id", 21 | "attr": { 22 | "type": "Number", 23 | "column": "icflav_id", 24 | "isPrimaryKey": true 25 | } 26 | }, 27 | { 28 | "name": "name", 29 | "attr": { 30 | "type": "String", 31 | "column": "icflav_name", 32 | "isNaturalKey": true 33 | } 34 | }, 35 | { 36 | "name": "description", 37 | "attr": { 38 | "type": "String", 39 | "column": "icflav_description" 40 | } 41 | }, 42 | { 43 | "name": "calories", 44 | "attr": { 45 | "type": "Number", 46 | "column": "icflav_calories" 47 | } 48 | } 49 | ], 50 | "isSystem": true 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/teprjtask.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | select xt.create_table('teprjtask', 'te'); 4 | select xt.add_column('teprjtask','teprjtask_id', 'serial', 'not null', 'te'); 5 | select xt.add_column('teprjtask','teprjtask_cust_id', 'integer', '', 'te'); 6 | select xt.add_column('teprjtask','teprjtask_rate', 'numeric', '', 'te'); 7 | select xt.add_column('teprjtask','teprjtask_item_id', 'integer', '', 'te'); 8 | select xt.add_column('teprjtask','teprjtask_prjtask_id', 'integer', '', 'te'); 9 | select xt.add_column('teprjtask','teprjtask_curr_id', 'integer', '', 'te'); 10 | select xt.add_primary_key('teprjtask', 'teprjtask_id', 'te'); 11 | select xt.add_constraint('teprjtask', 'teprjtask_teprjtask_curr_id_fkey','foreign key (teprjtask_curr_id) references curr_symbol (curr_id)', 'te'); 12 | select xt.add_constraint('teprjtask', 'teprjtask_teprjtask_item_id_fkey','foreign key (teprjtask_item_id) references item (item_id)', 'te'); 13 | --select xt.add_constraint('teprjtask', 'teprjtask_teprjtask_prjtask_id_fkey','foreign key (teprjtask_prjtask_id) references prjtask (prjtask_id) ', 'te'); 14 | select xt.add_constraint('teprjtask', 'teprjtask_teprjtask_prjtask_id_key','unique (teprjtask_prjtask_id )', 'te'); 15 | alter table te.teprjtask drop constraint if exists teprjtask_teprjtask_prjtask_id_fkey; 16 | alter table te.teprjtask alter column teprjtask_prjtask_id set not null; 17 | 18 | comment on table te.teprjtask is 'Time Expense Project Task'; 19 | -------------------------------------------------------------------------------- /source/bi_open/node-datasource/olapcatalog/olapcatalog.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, strict:true, trailing:true, white:true */ 3 | /*global X:true, _:true, issue:true */ 4 | 5 | (function () { 6 | "use strict"; 7 | 8 | /** 9 | OLAP Catalog Object 10 | 11 | @class 12 | @extends X.Object 13 | */ 14 | X.olapCatalog = X.Object.extend({ 15 | 16 | className: "X.olapCatalog", 17 | hostname: X.options.biServer.bihost || "localhost", 18 | port: X.options.biServer.port || 8080, 19 | 20 | xmlaConnect: new X.xmla.Xmla({ 21 | async: true, 22 | properties: { 23 | DataSourceInfo: "Provider=Mondrian;DataSource=Pentaho", 24 | Catalog: X.options.biServer.catalog || "xTuple", 25 | }, 26 | listeners: { 27 | events: X.xmla.Xmla.EVENT_ERROR, 28 | handler: function (eventName, eventData, xmla) { 29 | X.log( 30 | "xmla error occurred: " + eventData.exception.message + " (" + eventData.exception.code + ")" + 31 | (eventData.exception.code === X.xmla.Xmla.Exception.HTTP_ERROR_CDE ? 32 | "\nstatus: " + eventData.exception.data.status + "; statusText: " + eventData.exception.data.statusText 33 | : "") 34 | ); 35 | } 36 | } 37 | }) 38 | }); 39 | }()); -------------------------------------------------------------------------------- /source/time_expense/database/source/te/trigger_functions/tehead.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.triggertehead() RETURNS "trigger" AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | _r RECORD; 6 | _sense INTEGER := 0; 7 | 8 | BEGIN 9 | 10 | -- Determine whether we are adding or subtracting totals 11 | IF (TG_OP = 'UPDATE') THEN 12 | IF (OLD.tehead_status = 'O' AND NEW.tehead_status = 'A') THEN 13 | -- Approving so add 14 | _sense := 1; 15 | ELSIF (OLD.tehead_status = 'A' AND NEW.tehead_status = 'O') THEN 16 | -- Unapproving so subtract 17 | _sense := -1; 18 | END IF; 19 | END IF; 20 | 21 | IF (_sense != 0) THEN 22 | -- Loop thru all lines of the sheet and update project 23 | FOR _r in 24 | SELECT teitem_prjtask_id, teitem_type, teitem_qty, teitem_total 25 | FROM te.teitem 26 | WHERE teitem_tehead_id = NEW.tehead_id 27 | 28 | LOOP 29 | IF (_r.teitem_type = 'T') THEN 30 | UPDATE prjtask SET 31 | prjtask_hours_actual = prjtask_hours_actual + _r.teitem_qty * _sense 32 | WHERE prjtask_id = _r.teitem_prjtask_id; 33 | ELSE 34 | UPDATE prjtask SET 35 | prjtask_exp_actual = prjtask_exp_actual + _r.teitem_total * _sense 36 | WHERE prjtask_id = _r.teitem_prjtask_id; 37 | END IF; 38 | 39 | END LOOP; 40 | END IF; 41 | 42 | RETURN NEW; 43 | END; 44 | $$ LANGUAGE 'plpgsql'; 45 | -------------------------------------------------------------------------------- /source/bi_open/client/models/static.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XM:true, Backbone:true, _:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | // These are hard coded collections that may be turned into tables at a later date 10 | var i, 11 | date = new Date(); 12 | 13 | // Month for End Period 14 | XM.EndMonthModel = Backbone.Model.extend({ 15 | }); 16 | XM.EndMonthCollection = Backbone.Collection.extend({ 17 | model: XM.EndMonthModel 18 | }); 19 | XM.endMonths = new XM.EndMonthCollection(); 20 | XM.endMonths.add(new XM.EndMonthModel({id: "current", name: "Current"})); 21 | for (i = 12; i >= 1; i--) { 22 | var monthFormat = i < 10 ? "0" + i : "" + i; 23 | var month = new XM.EndMonthModel({id: monthFormat, name: monthFormat}); 24 | XM.endMonths.add(month); 25 | } 26 | 27 | // Year (for End Period 28 | date.setYear(Number(date.getFullYear()) + 1); 29 | XM.EndYearModel = Backbone.Model.extend({ 30 | }); 31 | XM.EndYearCollection = Backbone.Collection.extend({ 32 | model: XM.EndYearModel 33 | }); 34 | XM.endYears = new XM.EndYearCollection(); 35 | XM.endYears.add(new XM.EndYearModel({id: "current", name: "Current"})); 36 | for (i = Number(date.getFullYear()); i >= 2000; i--) { 37 | var yearFormat = "" + i; 38 | var year = new XM.EndYearModel({id: yearFormat, name: yearFormat}); 39 | XM.endYears.add(year); 40 | } 41 | 42 | }()); 43 | -------------------------------------------------------------------------------- /test/lib/test_runner.js: -------------------------------------------------------------------------------- 1 | /*jshint trailing:true, white:true, indent:2, strict:true, curly:true, 2 | immed:true, eqeqeq:true, forin:true, latedef:true, 3 | newcap:true, noarg:true, undef:true */ 4 | /*global XT:true, XM:true, XV:true, exports:true, describe:true, it:true, 5 | require:true, __dirname:true, console:true */ 6 | 7 | (function () { 8 | "use strict"; 9 | 10 | var fs = require('fs'), 11 | _ = require("underscore"), 12 | path = require('path'), 13 | extDirs = _.filter(fs.readdirSync(path.join(__dirname, "..")), function (filename) { 14 | return fs.statSync(path.join(__dirname, "..", filename)).isDirectory() && 15 | !_.contains(["lib", "build", "routes"], filename); 16 | }), 17 | specFiles = _.flatten(_.map(extDirs, function (dir) { 18 | var specDir = path.join(__dirname, "..", dir, "specs"), 19 | files = _.filter(fs.readdirSync(specDir), function (filename) { 20 | // filter out .swp files, etc. 21 | return path.extname(filename) === '.js'; 22 | }), 23 | filePaths = _.map(files, function (filename) { 24 | return path.join(__dirname, "..", dir, "specs", filename); 25 | }); 26 | 27 | return filePaths; 28 | })), 29 | specs = _.map(specFiles, function (specFile) { 30 | var fileContents = require(specFile); 31 | fileContents.spec.loginDataPath = path.join(__dirname, "login_data.js"); 32 | return fileContents; 33 | }), 34 | runSpec = require("../../../xtuple/test/lib/runner_engine").runSpec; 35 | 36 | _.each(specs, runSpec); 37 | 38 | }()); 39 | 40 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/views/workspace.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XV:true, Backbone:true, enyo:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.icecream.initWorkspace = function () { 10 | 11 | var extensions = [ 12 | {kind: "onyx.GroupboxHeader", container: "mainGroup", content: "_iceCreamFlavor".loc()}, 13 | {kind: "XV.IceCreamFlavorPicker", container: "mainGroup", attr: "favoriteFlavor" } 14 | ]; 15 | XV.appendExtension("XV.ContactWorkspace", extensions); 16 | 17 | enyo.kind({ 18 | name: "XV.IceCreamFlavorWorkspace", 19 | kind: "XV.Workspace", 20 | title: "_iceCreamFlavor".loc(), 21 | model: "XM.IceCreamFlavor", 22 | components: [ 23 | {kind: "Panels", arrangerKind: "CarouselArranger", 24 | fit: true, components: [ 25 | {kind: "XV.Groupbox", name: "mainPanel", components: [ 26 | {kind: "onyx.GroupboxHeader", content: "_overview".loc()}, 27 | {kind: "XV.ScrollableGroupbox", name: "mainGroup", classes: "in-panel", components: [ 28 | {kind: "XV.InputWidget", attr: "name"}, 29 | {kind: "XV.InputWidget", attr: "description"}, 30 | {kind: "XV.NumberWidget", attr: "calories"} 31 | ]} 32 | ]} 33 | ]} 34 | ] 35 | }); 36 | 37 | XV.registerModelWorkspace("XM.IceCreamFlavor", "XV.IceCreamFlavorWorkspace"); 38 | }; 39 | }()); 40 | -------------------------------------------------------------------------------- /source/time_expense/client/postbooks.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XV:true, enyo:true*/ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initPostbooks = function () { 9 | var relevantPrivileges, 10 | projectPanels, 11 | setupPanels; 12 | 13 | // .......................................................... 14 | // APPLICATION 15 | // 16 | 17 | setupPanels = [ 18 | {name: "departmentList", kind: "XV.DepartmentList"}, 19 | {name: "expenseCategoryList", kind: "XV.ExpenseCategoryList"}, 20 | {name: "shiftList", kind: "XV.ShiftList"} 21 | ]; 22 | XT.app.$.postbooks.appendPanels("setup", setupPanels); 23 | 24 | projectPanels = [ 25 | {name: "worksheetList", kind: "XV.WorksheetList"}, 26 | {name: "employeeList", kind: "XV.EmployeeList"}, 27 | {name: "employeeGroupList", kind: "XV.EmployeeGroupList"} 28 | ]; 29 | 30 | XT.app.$.postbooks.appendPanels("project", projectPanels); 31 | 32 | relevantPrivileges = [ 33 | "MaintainDepartments", 34 | "MaintainEmployees", 35 | "MaintainShifts", 36 | "ViewEmployees", 37 | "MaintainTimeExpenseOthers", 38 | "MaintainTimeExpenseSelf", 39 | "MaintainTimeExpense", 40 | "CanViewRates", 41 | "MaintainEmpCostAll", 42 | "CanApprove", 43 | "allowInvoicing", 44 | "allowVouchering", 45 | "PostTimeSheets", 46 | "ViewTimeExpenseHistory" 47 | ]; 48 | XT.session.addRelevantPrivileges("setup", relevantPrivileges); 49 | 50 | }; 51 | 52 | }()); 53 | -------------------------------------------------------------------------------- /source/time_expense/database/source/manifest.js: -------------------------------------------------------------------------------- 1 | { 2 | "name": "time_expense", 3 | "version": "", 4 | "comment": "Time Expense Management extension", 5 | "loadOrder": 100, 6 | "dependencies": ["project"], 7 | "databaseScripts": [ 8 | "te/schema/create_te_schema.sql", 9 | "te/functions/calcrate.sql", 10 | "te/functions/copyitem.sql", 11 | "te/functions/invoicesheets.sql", 12 | "te/functions/postsheet.sql", 13 | "te/functions/sheetstate.sql", 14 | "te/functions/unnest.sql", 15 | "te/functions/vouchersheet.sql", 16 | "te/trigger_functions/tehead.sql", 17 | "te/trigger_functions/teitem.sql", 18 | "te/trigger_functions/teprj.sql", 19 | "xt/trigger_functions/teitem_did_change.sql", 20 | "te/tables/tecustrate.sql", 21 | "te/tables/teemp.sql", 22 | "te/tables/teexp.sql", 23 | "te/tables/tehead.sql", 24 | "te/tables/teitem.sql", 25 | "te/tables/teprj.sql", 26 | "te/tables/teprjtask.sql", 27 | "xt/functions/te_total_hours.sql", 28 | "xt/functions/te_total_expenses.sql", 29 | "xt/functions/te_to_invoice.sql", 30 | "xt/functions/te_to_voucher.sql", 31 | "xt/functions/te_invoiced_value.sql", 32 | "xt/functions/te_posted_value.sql", 33 | "xt/functions/te_vouchered_value.sql", 34 | "xt/functions/te_invoiced_state.sql", 35 | "xt/functions/te_posted_state.sql", 36 | "xt/functions/te_vouchered_state.sql", 37 | "xt/views/prjtaskinfo.sql", 38 | "xt/views/tecustrateinfo.sql", 39 | "xt/views/teprjinfo.sql", 40 | "xt/views/teprjtaskinfo.sql", 41 | "xt/views/teexpinfo.sql", 42 | "xt/views/teheadinfo.sql", 43 | "xt/views/teiteminfo.sql", 44 | "xm/javascript/project.sql", 45 | "xm/javascript/worksheet.sql", 46 | "priv.sql" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /source/time_expense/client/models/customer.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.timeExpense.initCustomerModels = function () { 10 | 11 | var _proto = XM.Customer.prototype, 12 | _bindEvents = _proto.bindEvents, 13 | _statusDidChange = _proto.statusDidChange, 14 | _specifiedSetReadOnly = function () { 15 | var spec = this.get("isSpecifiedRate"); 16 | this.setReadOnly("billingRate", !spec); 17 | this.setReadOnly("billingCurrency", !spec); 18 | }; 19 | 20 | XM.Customer = XM.Customer.extend({ 21 | 22 | bindEvents: function () { 23 | _bindEvents.apply(this, arguments); 24 | this.on('change:isSpecifiedRate', this.isSpecifiedRateDidChange); 25 | }, 26 | 27 | isSpecifiedRateDidChange: function () { 28 | var spec = this.get("isSpecifiedRate"); 29 | if (spec) { 30 | this.set("billingRate", 0); 31 | this.set("billingCurrency", XT.baseCurrency()); 32 | } else { 33 | this.set("billingRate", null); 34 | this.set("billingCurrency", null); 35 | } 36 | _specifiedSetReadOnly.apply(this); 37 | }, 38 | 39 | statusDidChange: function () { 40 | _statusDidChange.apply(this, arguments); 41 | var K = XM.Model, 42 | status = this.getStatus(); 43 | if (status === K.READY_NEW || status === K.READY_CLEAN) { 44 | _specifiedSetReadOnly.apply(this); 45 | } 46 | } 47 | 48 | }); 49 | 50 | }; 51 | 52 | }()); 53 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/tecustrateinfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.tecustrateinfo', $$ 2 | select tecustrate.*, 3 | case when tecustrate_curr_id is not null then true 4 | else false 5 | end as tecustrate_specified_rate 6 | from te.tecustrate 7 | $$, false); 8 | 9 | -- The null handling below is to ensure compatibility with the fickle implementation in the desktop client 10 | 11 | create or replace rule "_INSERT" as on insert to xt.tecustrateinfo 12 | do instead nothing; 13 | 14 | create or replace rule "_INSERT_NOT_NULL" as on insert to xt.tecustrateinfo 15 | where new.tecustrate_curr_id is not null do 16 | 17 | insert into te.tecustrate ( 18 | tecustrate_id, 19 | tecustrate_cust_id, 20 | tecustrate_rate, 21 | tecustrate_curr_id 22 | ) values ( 23 | coalesce(new.tecustrate_id, nextval('te.tecustrate_tecustrate_id_seq')), 24 | new.tecustrate_cust_id, 25 | new.tecustrate_rate, 26 | new.tecustrate_curr_id 27 | ); 28 | 29 | create or replace rule "_UPDATE" as on update to xt.tecustrateinfo 30 | do instead nothing; 31 | 32 | create or replace rule "_UPDATE_NULL" as on update to xt.tecustrateinfo 33 | where new.tecustrate_curr_id is null do 34 | 35 | delete from te.tecustrate where tecustrate_id = old.tecustrate_id; 36 | 37 | create or replace rule "_UPDATE_NOT_NULL" as on update to xt.tecustrateinfo 38 | where new.tecustrate_curr_id is not null do 39 | 40 | update te.tecustrate set 41 | tecustrate_cust_id=new.tecustrate_cust_id, 42 | tecustrate_rate=new.tecustrate_rate, 43 | tecustrate_curr_id=new.tecustrate_curr_id 44 | where tecustrate_id = old.tecustrate_id; 45 | 46 | create or replace rule "_DELETE" as on delete to xt.tecustrateinfo do instead 47 | 48 | delete from te.tecustrate where tecustrate_id = old.tecustrate_id; 49 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/client/models/ice_cream_flavor.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.icecream.initModels = function () { 10 | XM.IceCreamFlavor = XM.Document.extend({ 11 | 12 | recordType: "XM.IceCreamFlavor", 13 | 14 | documentKey: "name", // the natural key 15 | 16 | idAttribute: "name", // the natural key 17 | 18 | bindEvents: function () { 19 | XM.Document.prototype.bindEvents.apply(this, arguments); 20 | this.on('change:calories', this.caloriesDidChange); 21 | }, 22 | 23 | caloriesDidChange: function () { 24 | var calories = this.get("calories"), 25 | name = this.get("name"); 26 | 27 | if (calories < 450 && name.indexOf('LITE ') !== 0) { 28 | // add the word lite as applicable 29 | this.set("name", 'LITE ' + name); 30 | } else if (calories >= 450 && name.indexOf('LITE ') === 0) { 31 | // get rid of the word lite if it's not applicable 32 | this.set("name", name.substring(5)); 33 | } 34 | }, 35 | 36 | validate: function (attributes) { 37 | var params = {}; 38 | if (attributes.calories <= 450 && attributes.name.indexOf('LITE ') !== 0) { 39 | return XT.Error.clone('icecream3001', { params: params }); 40 | } 41 | // if our custom validation passes, then just test the usual validation 42 | return XM.Document.prototype.validate.apply(this, arguments); 43 | } 44 | 45 | }); 46 | 47 | XM.IceCreamFlavorCollection = XM.Collection.extend({ 48 | model: XM.IceCreamFlavor 49 | }); 50 | }; 51 | }()); 52 | -------------------------------------------------------------------------------- /test/build/build_app.js: -------------------------------------------------------------------------------- 1 | /*jshint trailing:true, white:true, indent:2, strict:true, curly:true, 2 | immed:true, eqeqeq:true, forin:true, latedef:true, 3 | newcap:true, noarg:true, undef:true */ 4 | /*global XT:true, describe:true, it:true, require:true, __dirname:true, after:true */ 5 | 6 | var buildAll = require('../../../xtuple/scripts/lib/build_all'), 7 | _ = require('underscore'), 8 | assert = require('chai').assert, 9 | path = require('path'); 10 | 11 | (function () { 12 | "use strict"; 13 | describe('The database build tool', function () { 14 | this.timeout(100 * 60 * 1000); 15 | 16 | var loginData = require(path.join(__dirname, "../lib/login_data.js")).data, 17 | databaseName = loginData.org, 18 | extensions = ["time_expense"], 19 | datasource = require('../../../xtuple/node-datasource/lib/ext/datasource').dataSource, 20 | config = require(path.join(__dirname, "../../../xtuple/node-datasource/config.js")), 21 | creds = config.databaseServer; 22 | 23 | creds.host = creds.hostname; // adapt our lingo to node-postgres lingo 24 | 25 | it('should build without error on a brand-new database', function (done) { 26 | buildAll.build({ 27 | database: databaseName, 28 | initialize: true, 29 | populateData: true, 30 | source: path.join(__dirname, "../../../xtuple/foundation-database/postbooks_demo_data.sql") 31 | }, function (err, res) { 32 | assert.isNull(err); 33 | done(); 34 | }); 35 | }); 36 | 37 | _.each(extensions, function (extension) { 38 | it('should build the ' + extension + ' extension', function (done) { 39 | buildAll.build({ 40 | database: databaseName, 41 | extension: path.join(__dirname, "../../source", extension) 42 | }, function (err, res) { 43 | assert.isNull(err); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | }); 49 | }()); 50 | 51 | 52 | -------------------------------------------------------------------------------- /sample/xtuple-ice-cream/test/ice_cream_flavor.js: -------------------------------------------------------------------------------- 1 | /*jshint trailing:true, white:true, indent:2, strict:true, curly:true, 2 | immed:true, eqeqeq:true, forin:true, latedef:true, node:true, 3 | newcap:true, noarg:true, undef:true */ 4 | /*global XT:true, XM:true, XV:true, describe:true, it:true, before:true, assert:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | var assert = require("chai").assert; 10 | 11 | var spec = { 12 | recordType: "XM.IceCreamFlavor", 13 | collectionType: "XM.IceCreamFlavorCollection", 14 | cacheName: "XM.iceCreamFlavors", 15 | listKind: "XV.IceCreamFlavorList", 16 | instanceOf: "XM.Document", 17 | isLockable: true, 18 | idAttribute: "name", 19 | enforceUpperKey: true, 20 | attributes: ["name", "description", "calories"], 21 | extensions: ["xtuple-ice-cream"], 22 | createHash: { 23 | name: "VANILLA" + Math.random(), 24 | calories: 1200 25 | }, 26 | updateHash: { 27 | calories: 1400 28 | }, 29 | privileges: { 30 | createUpdateDelete: "MaintainIceCreamFlavors", 31 | read: true 32 | } 33 | }; 34 | var additionalTests = function () { 35 | describe("Ice cream flavor business logic", function () { 36 | var model; 37 | 38 | before(function (done) { 39 | model = new XM.IceCreamFlavor(); 40 | model.once("status:READY_NEW", function () { 41 | done(); 42 | }); 43 | model.initialize(null, {isNew: true}); 44 | }); 45 | it("should update the description to and from LITE", function () { 46 | model.set({name: "VANILLA"}); 47 | assert.equal(model.get("name").substring(0, 7), "VANILLA"); 48 | model.set("calories", 200); 49 | assert.equal(model.get("name").substring(0, 7), "LITE VA"); 50 | model.set("calories", 1200); 51 | assert.equal(model.get("name").substring(0, 7), "VANILLA"); 52 | }); 53 | }); 54 | }; 55 | 56 | exports.spec = spec; 57 | exports.additionalTests = additionalTests; 58 | }()); 59 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/tehead.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | -- create a sequence for timesheet numbers if one doesn't exist 4 | do $$ 5 | 6 | var res, 7 | sql = "select * from pg_class c " + 8 | "join pg_namespace n on n.oid = c.relnamespace " + 9 | "where c.relkind = 'S' and n.nspname = 'te' and c.relname = 'timesheet_seq';"; 10 | 11 | res = plv8.execute(sql); 12 | if (!res.length) { 13 | sql = "create sequence te.timesheet_seq start 1"; 14 | plv8.execute(sql); 15 | } 16 | 17 | $$ language plv8; 18 | 19 | select xt.create_table('tehead', 'te'); 20 | 21 | -- remove old trigger if any 22 | 23 | drop trigger if exists teheadtrigger on te.tehead; 24 | 25 | select xt.add_column('tehead','tehead_id', 'serial', 'not null', 'te'); 26 | select xt.add_column('tehead','tehead_number', 'text', $$default nextval('te.timesheet_seq'::regclass)$$, 'te'); 27 | select xt.add_column('tehead','tehead_weekending', 'date', '', 'te'); 28 | select xt.add_column('tehead','tehead_lastupdated', 'timestamp without time zone', 'not null default now()', 'te'); 29 | select xt.add_column('tehead','tehead_notes', 'text', '', 'te'); 30 | select xt.add_column('tehead','tehead_status', 'character(1)', $$not null default 'O'::bpchar$$, 'te'); 31 | select xt.add_column('tehead','tehead_emp_id', 'integer', '', 'te'); 32 | select xt.add_column('tehead','tehead_warehous_id', 'integer', '', 'te'); 33 | select xt.add_column('tehead','tehead_username', 'text', '', 'te'); 34 | select xt.add_primary_key('tehead', 'tehead_id', 'te'); 35 | select xt.execute_query('alter table te.tehead alter column tehead_username set default geteffectivextuser()'); 36 | select xt.add_constraint('tehead', 'tehead_tehead_status_check', $$check (tehead_status = any (array['O'::bpchar, 'A'::bpchar, 'C'::bpchar]))$$, 'te'); 37 | 38 | comment on table te.tehead is 'Time Expense Worksheet Header'; 39 | 40 | -- create trigger 41 | 42 | create trigger teheadtrigger after insert or update on te.tehead for each row execute procedure te.triggertehead(); -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/teheadinfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.teheadinfo', $$ 2 | 3 | select tehead.*, 4 | xt.te_total_hours(tehead_id) as total_hours, 5 | xt.te_total_expenses(tehead_id) as total_expenses, 6 | xt.te_to_voucher(tehead_id) as to_voucher, 7 | xt.te_to_invoice(tehead_id) as to_invoice, 8 | xt.te_invoiced_state(tehead_id) as invoiced, 9 | xt.te_vouchered_state(tehead_id) as vouchered, 10 | xt.te_posted_state(tehead_id) as posted, 11 | xt.te_invoiced_value(tehead_id) as invoiced_value, 12 | xt.te_vouchered_value(tehead_id) as vouchered_value, 13 | xt.te_posted_value(tehead_id) as posted_value, 14 | basecurrid() as curr_id 15 | from te.tehead; 16 | 17 | $$, false); 18 | 19 | 20 | create or replace rule "_INSERT" as on insert to xt.teheadinfo do instead 21 | 22 | insert into te.tehead ( 23 | tehead_id, 24 | tehead_number, 25 | tehead_weekending, 26 | tehead_lastupdated, 27 | tehead_notes, 28 | tehead_status, 29 | tehead_emp_id, 30 | tehead_warehous_id, 31 | tehead_username 32 | ) values ( 33 | coalesce(new.tehead_id, nextval('te.timesheet_seq'::regclass)), 34 | new.tehead_number, 35 | new.tehead_weekending, 36 | coalesce(new.tehead_lastupdated, now()), 37 | new.tehead_notes, 38 | coalesce(new.tehead_status, 'O'), 39 | new.tehead_emp_id, 40 | new.tehead_warehous_id, 41 | coalesce(new.tehead_username, geteffectivextuser()) 42 | ); 43 | 44 | create or replace rule "_UPDATE" as on update to xt.teheadinfo do instead 45 | 46 | update te.tehead set 47 | tehead_number = new.tehead_number, 48 | tehead_weekending = new.tehead_weekending, 49 | tehead_lastupdated = new.tehead_lastupdated, 50 | tehead_notes = new.tehead_notes, 51 | tehead_status = new.tehead_status, 52 | tehead_emp_id = new.tehead_emp_id, 53 | tehead_warehous_id = new.tehead_warehous_id, 54 | tehead_username = new.tehead_username 55 | where tehead_id = old.tehead_id; 56 | 57 | create or replace rule "_DELETE" as on delete to xt.teheadinfo do instead 58 | 59 | delete from te.tehead where tehead_id=old.tehead_id; 60 | -------------------------------------------------------------------------------- /source/time_expense/database/orm/ext/item.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "time_expense", 4 | "nameSpace": "XM", 5 | "type": "Item", 6 | "table": "xt.teexpinfo", 7 | "isExtension": true, 8 | "comment": "Extended by time_expense", 9 | "relations": [ 10 | { 11 | "column": "teexp_id", 12 | "inverse": "id" 13 | } 14 | ], 15 | "properties": [ 16 | { 17 | "name": "projectExpenseMethod", 18 | "attr": { 19 | "type": "String", 20 | "column": "teexp_method" 21 | } 22 | }, 23 | { 24 | "name": "projectExpenseCategory", 25 | "toOne": { 26 | "isNested": true, 27 | "type": "ExpenseCategory", 28 | "column": "teexp_expcat_id" 29 | } 30 | }, 31 | { 32 | "name": "projectExpenseLedgerAccount", 33 | "toOne": { 34 | "isNested": true, 35 | "type": "LedgerAccountRelation", 36 | "column": "teexp_accnt_id" 37 | } 38 | } 39 | ], 40 | "isSystem": true 41 | }, 42 | { 43 | "context": "time_expense", 44 | "nameSpace": "XM", 45 | "type": "ItemListItem", 46 | "table": "xt.teexpinfo", 47 | "isExtension": true, 48 | "comment": "Extended by Time Expense", 49 | "relations": [ 50 | { 51 | "column": "teexp_id", 52 | "inverse": "id" 53 | } 54 | ], 55 | "properties": [ 56 | { 57 | "name": "projectExpenseMethod", 58 | "attr": { 59 | "type": "String", 60 | "column": "teexp_method" 61 | } 62 | } 63 | ], 64 | "isSystem": true 65 | }, 66 | { 67 | "context": "time_expense", 68 | "nameSpace": "XM", 69 | "type": "ItemRelation", 70 | "table": "xt.teexpinfo", 71 | "isExtension": true, 72 | "comment": "Extended by Time Expense", 73 | "relations": [ 74 | { 75 | "column": "teexp_id", 76 | "inverse": "id" 77 | } 78 | ], 79 | "properties": [ 80 | { 81 | "name": "projectExpenseMethod", 82 | "attr": { 83 | "type": "String", 84 | "column": "teexp_method" 85 | } 86 | } 87 | ], 88 | "isSystem": true 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/sheetstate.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.sheetstate(INTEGER, CHAR(1)) RETURNS INTEGER AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | pTeheadId ALIAS FOR $1; 6 | pType ALIAS FOR $2; 7 | _state INTEGER := -1; 8 | 9 | BEGIN 10 | -- Check and return the process state of the sheet 11 | -- 1 = All processed 12 | -- 0 = Processing required 13 | -- -1 = Not Applicable 14 | 15 | IF (pType = 'I') THEN 16 | SELECT MIN(CASE teitem_invcitem_id IS NULL WHEN TRUE THEN 0 ELSE 1 END) INTO _state 17 | FROM te.teitem 18 | WHERE ((teitem_tehead_id=pTeheadId) 19 | AND (teitem_billable) 20 | AND (teitem_qty >= 0)); 21 | 22 | ELSIF (pType = 'V') THEN 23 | -- todo: why outer join then check teitem_type and vend_id is not null? 24 | SELECT MIN(CASE teitem_vodist_id IS NULL WHEN TRUE THEN 0 ELSE 1 END) INTO _state 25 | FROM te.tehead 26 | JOIN emp ON (tehead_emp_id=emp_id) 27 | LEFT OUTER JOIN te.teemp ON (emp_id=teemp_emp_id) 28 | LEFT OUTER JOIN te.teitem ON (teitem_tehead_id=tehead_id) 29 | LEFT OUTER JOIN vendinfo ON (UPPER(emp_code)=UPPER(vend_number)) 30 | WHERE ((teitem_tehead_id=pTeheadId) 31 | AND ((teitem_type = 'E' AND NOT teitem_prepaid) 32 | OR (teitem_type = 'T' AND COALESCE(teemp_contractor,false))) 33 | AND (vend_id IS NOT NULL) 34 | AND (teitem_qty > 0)); 35 | 36 | ELSIF (pType = 'P') THEN 37 | SELECT MIN(CASE teitem_posted WHEN FALSE THEN 0 ELSE 1 END) INTO _state 38 | FROM te.teitem 39 | JOIN te.tehead ON (teitem_tehead_id=tehead_id) 40 | LEFT JOIN te.teemp ON (tehead_emp_id=teemp_emp_id) 41 | WHERE ((teitem_tehead_id=pTeheadId) 42 | AND (teitem_type = 'T') 43 | AND (teemp_contractor IS NULL or teemp_contractor = false)); 44 | 45 | ELSE 46 | -- TODO: either make ErrorReporter::error find this or use xtuple 47 | RAISE EXCEPTION 'Unknown process type % [xtte: sheetstate, -2, %]', 48 | pType, pType; 49 | END IF; 50 | 51 | RETURN _state; 52 | 53 | END; 54 | $$ LANGUAGE 'plpgsql'; 55 | -------------------------------------------------------------------------------- /source/bi_open/client/models/analytic.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | /** 10 | @class 11 | @extends Backbone.Model 12 | */ 13 | XM.Analytic = Backbone.Model.extend({ 14 | 15 | url: '/queryOlap' 16 | }); 17 | 18 | // .......................................................... 19 | // COLLECTIONS 20 | // 21 | 22 | /** 23 | @class 24 | @extends Backbone.Collection 25 | */ 26 | XM.AnalyticCollection = Backbone.Collection.extend({ 27 | 28 | model: XM.Analytic, 29 | url: '/queryOlap', 30 | queryComplete: true, 31 | 32 | sync : function (method, model, options) { 33 | var results; 34 | 35 | function success(result) { 36 | results = result; 37 | if (options.success) { 38 | options.success(result); 39 | } 40 | } 41 | 42 | function error(result) { 43 | results = result; 44 | if (options.error) { 45 | options.error(result); 46 | } 47 | } 48 | 49 | switch (method) { 50 | case 'read': 51 | var that = this; 52 | this.setQueryComplete(false); 53 | XT.DataSource.callRoute( 54 | "queryOlap?mdx=" + options.data.mdx, 55 | {}, 56 | {success: function (result) { 57 | that.setQueryComplete(true); 58 | results = result; 59 | if (options.success) { 60 | options.success(model, result, options); 61 | } 62 | }, 63 | error: function (result) { 64 | that.setQueryComplete(true); 65 | results = result; 66 | if (options.error) { 67 | options.error(model, result, options); 68 | } 69 | } 70 | } 71 | ); 72 | return; 73 | 74 | default: 75 | if (options.error) { 76 | options.error("only read method is implemented"); 77 | } 78 | return; 79 | } 80 | }, 81 | 82 | getQueryComplete: function () { 83 | return this.queryComplete; 84 | }, 85 | 86 | setQueryComplete: function (value) { 87 | this.queryComplete = value; 88 | } 89 | 90 | }); 91 | }()); 92 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/trigger_functions/teitem_did_change.sql: -------------------------------------------------------------------------------- 1 | create or replace function xt.teitem_did_change() returns trigger as $$ 2 | /* Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | See www.xm.ple.com/CPAL for the full text of the software license. */ 4 | 5 | if (typeof XT === 'undefined') { 6 | plv8.execute("select xt.js_init();"); 7 | } 8 | 9 | var data = Object.create(XT.Data), 10 | sql, 11 | params, 12 | qry, 13 | rate; 14 | 15 | /* Populate employee cost if it wasn't already included */ 16 | if (NEW.teitem_type === 'T') { 17 | if (NEW.teitem_empcost === null) { 18 | sql = "update te.teitem set teitem_empcost = (" + 19 | " select te.calcrate(emp_wage, emp_wage_period) as cost " + 20 | " from te.tehead " + 21 | " join emp on tehead_emp_id = emp_id " + 22 | " where tehead_id = teitem_tehead_id)" + 23 | "where teitem_id = $1;" 24 | plv8.execute(sql, [NEW.teitem_id]); 25 | } 26 | 27 | if (!data.checkPrivilege("CanViewRates")) { 28 | if (NEW.teitem_billable) { 29 | sql = "select prj_number, " + 30 | " prjtask.obj_uuid as task_uuid, " + 31 | " emp_code, cust_number, item_number " + 32 | "from te.teitem " + 33 | " join prjtask on teitem_prjtask_id=prjtask_id " + 34 | " join prj on prjtask_prj_id=prj_id " + 35 | " join te.tehead on teitem_tehead_id=tehead_id " + 36 | " join emp on tehead_emp_id=emp_id " + 37 | " join custinfo on teitem_cust_id=cust_id " + 38 | " join item on teitem_item_id=item_id " + 39 | "where teitem_id=$1 "; 40 | 41 | row = plv8.execute(sql, [NEW.teitem_id])[0]; 42 | params = { 43 | isTime: true, 44 | taskId: row.task_uuid, 45 | projectId: row.prj_number, 46 | employeeId: row.emp_code, 47 | customerId: row.cust_number, 48 | itemId: row.item_number 49 | }; 50 | rate = XM.Worksheet.getBillingRate(params).rate; 51 | } else { 52 | rate = 0; 53 | } 54 | sql = "update te.teitem set teitem_rate = $1, teitem_total = $1 * teitem_qty " + 55 | "where teitem_id = $2;" 56 | plv8.execute(sql, [rate, NEW.teitem_id]); 57 | } 58 | } 59 | return NEW; 60 | 61 | $$ language plv8; 62 | -------------------------------------------------------------------------------- /source/time_expense/client/en/strings.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | strict:true, trailing:true, white:true */ 4 | /*global XT:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | var lang = XT.stringsFor("en_US", { 10 | // ******** 11 | // Labels 12 | // ******** 13 | 14 | "_approve": "Approve", 15 | "_approved": "Approved", 16 | "_byAccount": "by Account", 17 | "_byCategory": "by Category", 18 | "_billing": "Billing", 19 | "_billable": "Billable", 20 | "_cost": "Cost", 21 | "_isContractor": "Contractor", 22 | "_custPo": "Cust. PO#", 23 | "_detail": "Detail", 24 | "_expense": "Expense", 25 | "_invoiced": "Invoiced", 26 | "_isSpecifiedRate": "Specified Rate", 27 | "_noInvoice": "No Invoice", 28 | "_noVoucher": "No Voucher", 29 | "_notUsed": "Not Used", 30 | "_TE": "Time Expense", 31 | "_tE": "Time Expense", 32 | "_posted": "Posted", 33 | "_prepaid": "Prepaid", 34 | "_projectBilling": "Project Billing", 35 | "_rate": "Rate", 36 | "_site": "Site", 37 | "_task": "Task", 38 | "_time": "Time", 39 | "_timeExpense": "Time Expense", 40 | "_unapprove": "Unapprove", 41 | "_unitCost": "Unit Cost", 42 | "_vouchered": "Vouchered", 43 | "_weekOf": "Week Of", 44 | "_workDate": "Work Date", 45 | "_worksheet": "Worksheet", 46 | "_worksheets": "Worksheets", 47 | 48 | // ******** 49 | // Messages 50 | // ******** 51 | "_postWorksheetFor": "Post Time Sheet for ", 52 | "_toProject": " to Project", 53 | "_closeWorksheet?": "Are you sure you want to close the worksheet? This action can not be undone.", 54 | 55 | // ******** 56 | // Privileges 57 | // ******** 58 | 59 | "_accessPPMExtension": "Access PPM Extension", 60 | "_canApprove": "Approve Worksheets", 61 | "_canViewRates": "Can View Billing Rates", 62 | "_maintainEmpCostAll": "Maintain All Employee Costs", 63 | "_maintainTimeExpense": "Delete and Close Worksheets", 64 | "_maintainTimeExpenseOthers": "Maintain All Worksheets", 65 | "_maintainTimeExpenseSelf": "Maintain Personal Worksheets", 66 | "_postTimeSheets": "Post Worksheets", 67 | "_viewTimeExpenseHistory": "View Worksheet History", 68 | "_allowInvoicing": "Invoice Worksheets", 69 | "_allowVouchering": "Voucher Worksheets" 70 | }); 71 | 72 | if (typeof exports !== 'undefined') { 73 | exports.language = lang; 74 | } 75 | }()); 76 | -------------------------------------------------------------------------------- /test/time_expense/specs/employeegroup.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XV:true, XT:true, _:true, console:true, XM:true, Backbone:true, require:true, assert:true, 5 | setTimeout:true, before:true, clearTimeout:true, exports:true, it:true, describe:true, beforeEach:true */ 6 | 7 | (function () { 8 | "use strict"; 9 | 10 | /** 11 | Employee Groups are used for categorizing groups of related Employees 12 | @class 13 | @alias EmployeeGroup 14 | @property {String} Name 15 | @property {String} Description 16 | */ 17 | var spec = { 18 | recordType: "XM.EmployeeGroup", 19 | collectionType: "XM.EmployeeGroupCollection", 20 | cacheName: null, 21 | listKind: "XV.EmployeeGroupList", 22 | instanceOf: "XM.Document", 23 | /** 24 | @member - 25 | @memberof EmployeeGroup 26 | @description Employee Groups are lockable. 27 | */ 28 | isLockable: true, 29 | /** 30 | @member - 31 | @memberof EmployeeGroup 32 | @description The ID attribute is "name", which will be automatically uppercased. 33 | */ 34 | idAttribute: "name", 35 | enforceUpperKey: true, 36 | attributes: ["id", "name", "description", "employees"], 37 | /** 38 | @member - 39 | @memberof EmployeeGroup 40 | @description Used in the Time and expense module 41 | */ 42 | extensions: ["project"], 43 | /** 44 | @member - 45 | @memberof EmployeeGroup 46 | @description Employee Groups can be read by users with "ViewEmployeeGroups" privilege and can be created, updated, 47 | or deleted by users with the "MaintainEmployeeGroups" privilege. 48 | */ 49 | /*privileges: { //22834 50 | createUpdateDelete: "MaintainEmployeeGroups", 51 | read: "ViewEmployeeGroups" 52 | },*/ 53 | createHash: { 54 | name: "TestEmployeeGroup" + Math.random(), 55 | description: "Test Employee Group" 56 | }, 57 | updatableField: "description" 58 | }; 59 | var additionalTests = function () { 60 | it.skip("Employees can be attached/detached to a new Employee Group", function () { 61 | }); 62 | it.skip("Employees can be attached/detached to an existing Employee Group", function () { 63 | }); 64 | it.skip("Employee Groups with Employees attached to them cannot be deleted", function () { 65 | }); 66 | }; 67 | exports.spec = spec; 68 | exports.additionalTests = additionalTests; 69 | }()); 70 | 71 | -------------------------------------------------------------------------------- /source/time_expense/client/widgets/parameter.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XM:true, XV:true, enyo:true*/ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initParameters = function () { 9 | 10 | // .......................................................... 11 | // WORKSHEET 12 | // 13 | 14 | enyo.kind({ 15 | name: "XV.WorksheetListParameters", 16 | kind: "XV.ParameterWidget", 17 | components: [ 18 | {kind: "onyx.GroupboxHeader", content: "_worksheet".loc()}, 19 | {name: "employee", label: "_employee".loc(), attr: "employee", 20 | defaultKind: "XV.EmployeeWidget"}, 21 | {name: "owner", label: "_owner".loc(), attr: "owner", 22 | defaultKind: "XV.UserAccountWidget"}, 23 | {kind: "onyx.GroupboxHeader", content: "_weekOf".loc()}, 24 | {name: "fromDate", label: "_fromDate".loc(), attr: "weekOf", operator: ">=", 25 | defaultKind: "XV.DateWidget"}, 26 | {name: "toDate", label: "_toDate".loc(), attr: "weekOf", operator: "<=", 27 | defaultKind: "XV.DateWidget"}, 28 | {kind: "onyx.GroupboxHeader", content: "_status".loc()}, 29 | {name: "isOpen", label: "_open".loc(), defaultKind: "XV.CheckboxWidget"}, 30 | {name: "isApproved", label: "_approved".loc(), defaultKind: "XV.CheckboxWidget"}, 31 | {name: "isClosed", label: "_closed".loc(), defaultKind: "XV.CheckboxWidget"}, 32 | {kind: "onyx.GroupboxHeader", content: "_employee".loc()}, 33 | {name: "manager", label: "_manager".loc(), attr: "employee.manager.code", 34 | defaultKind: "XV.EmployeeWidget"}, 35 | {name: "department", label: "_department".loc(), attr: "employee.department.number", 36 | defaultKind: "XV.DepartmentWidget"}, 37 | {name: "shift", label: "_shift".loc(), attr: "employee.shift.number", 38 | defaultKind: "XV.ShiftWidget"} 39 | ], 40 | getParameters: function () { 41 | var params = this.inherited(arguments), 42 | param = {}, 43 | value = []; 44 | if (this.$.isOpen.getValue()) { 45 | value.push('O'); 46 | } 47 | if (this.$.isApproved.getValue()) { 48 | value.push('A'); 49 | } 50 | if (this.$.isClosed.getValue()) { 51 | value.push('C'); 52 | } 53 | if (value.length) { 54 | param.attribute = "worksheetStatus"; 55 | param.operator = "ANY"; 56 | param.value = value; 57 | params.push(param); 58 | } 59 | return params; 60 | } 61 | }); 62 | }; 63 | 64 | }()); 65 | -------------------------------------------------------------------------------- /source/bi_open/database/orm/models/chart.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "xtuple", 4 | "nameSpace": "XM", 5 | "type": "UserBiChart", 6 | "table": "bi_open.usrbichart", 7 | "idSequenceName": "bi_open.usrbichart_usrbichart_id_seq", 8 | "comment": "User Chart Map", 9 | "privileges": { 10 | "all": { 11 | "create": true, 12 | "read": true, 13 | "update": true, 14 | "delete": true 15 | } 16 | }, 17 | "properties": [ 18 | { 19 | "name": "id", 20 | "attr": { 21 | "type": "Number", 22 | "column": "usrbichart_id", 23 | "isPrimaryKey": true 24 | } 25 | }, 26 | { 27 | "name": "uuid", 28 | "attr": { 29 | "type": "String", 30 | "column": "obj_uuid", 31 | "isNaturalKey": true 32 | } 33 | }, 34 | { 35 | "name": "order", 36 | "attr": { 37 | "type": "Number", 38 | "column": "usrbichart_order" 39 | } 40 | }, 41 | { 42 | "name": "chartname", 43 | "attr": { 44 | "type": "String", 45 | "column": "usrbichart_chart" 46 | } 47 | }, 48 | { 49 | "name": "username", 50 | "attr": { 51 | "type": "String", 52 | "column": "usrbichart_usr_username" 53 | } 54 | }, 55 | { 56 | "name": "extension", 57 | "attr": { 58 | "type": "String", 59 | "column": "usrbichart_ext_name" 60 | } 61 | }, 62 | { 63 | "name": "filter", 64 | "attr": { 65 | "type": "String", 66 | "column": "usrbichart_filter_option" 67 | } 68 | }, 69 | { 70 | "name": "groupBy", 71 | "attr": { 72 | "type": "String", 73 | "column": "usrbichart_groupby_option" 74 | } 75 | }, 76 | { 77 | "name": "measure", 78 | "attr": { 79 | "type": "String", 80 | "column": "usrbichart_measure" 81 | } 82 | }, 83 | { 84 | "name": "chartType", 85 | "attr": { 86 | "type": "String", 87 | "column": "usrbichart_charttype" 88 | } 89 | }, 90 | { 91 | "name": "dimension", 92 | "attr": { 93 | "type": "String", 94 | "column": "usrbichart_dimension" 95 | } 96 | }, 97 | { 98 | "name": "uuidFilter", 99 | "attr": { 100 | "type": "String", 101 | "column": "usrbichart_uuid_filter" 102 | } 103 | } 104 | ], 105 | "isSystem": true 106 | } 107 | ] 108 | -------------------------------------------------------------------------------- /source/time_expense/database/source/xt/views/teiteminfo.sql: -------------------------------------------------------------------------------- 1 | select xt.create_view('xt.teiteminfo', $$ 2 | 3 | select teitem.*, 4 | teitem_qty * teitem_empcost as hourly_total, 5 | basecurrid() as hourly_curr_id 6 | from te.teitem; 7 | 8 | $$, false); 9 | 10 | 11 | create or replace rule "_INSERT" as on insert to xt.teiteminfo do instead 12 | 13 | insert into te.teitem ( 14 | teitem_id, 15 | teitem_tehead_id, 16 | teitem_linenumber, 17 | teitem_type, 18 | teitem_workdate, 19 | teitem_cust_id, 20 | teitem_vend_id, 21 | teitem_po, 22 | teitem_item_id, 23 | teitem_qty, 24 | teitem_rate, 25 | teitem_total, 26 | teitem_prjtask_id, 27 | teitem_lastupdated, 28 | teitem_billable, 29 | teitem_prepaid, 30 | teitem_notes, 31 | teitem_posted, 32 | teitem_curr_id, 33 | teitem_uom_id, 34 | teitem_invcitem_id, 35 | teitem_vodist_id, 36 | teitem_postedvalue, 37 | teitem_empcost, 38 | obj_uuid 39 | ) values ( 40 | coalesce(new.teitem_id, nextval('te.timesheet_seq'::regclass)), 41 | new.teitem_tehead_id, 42 | new.teitem_linenumber, 43 | new.teitem_type, 44 | new.teitem_workdate, 45 | new.teitem_cust_id, 46 | new.teitem_vend_id, 47 | new.teitem_po, 48 | new.teitem_item_id, 49 | new.teitem_qty, 50 | new.teitem_rate, 51 | new.teitem_total, 52 | new.teitem_prjtask_id, 53 | coalesce(new.teitem_lastupdated, ('now'::text)::timestamp(6) with time zone), 54 | new.teitem_billable, 55 | new.teitem_prepaid, 56 | new.teitem_notes, 57 | coalesce(new.teitem_posted, false), 58 | new.teitem_curr_id, 59 | new.teitem_uom_id, 60 | new.teitem_invcitem_id, 61 | new.teitem_vodist_id, 62 | coalesce(new.teitem_postedvalue, 0), 63 | new.teitem_empcost, 64 | new.obj_uuid 65 | ); 66 | 67 | create or replace rule "_UPDATE" as on update to xt.teiteminfo do instead 68 | 69 | update te.teitem set 70 | teitem_linenumber=new.teitem_linenumber, 71 | teitem_workdate=new.teitem_workdate, 72 | teitem_cust_id=new.teitem_cust_id, 73 | teitem_vend_id=new.teitem_vend_id, 74 | teitem_po=new.teitem_po, 75 | teitem_item_id=new.teitem_item_id, 76 | teitem_qty=new.teitem_qty, 77 | teitem_rate=new.teitem_rate, 78 | teitem_total=new.teitem_total, 79 | teitem_prjtask_id=new.teitem_prjtask_id, 80 | teitem_lastupdated=new.teitem_lastupdated, 81 | teitem_billable=new.teitem_billable, 82 | teitem_prepaid=new.teitem_prepaid, 83 | teitem_notes=new.teitem_notes, 84 | teitem_posted=new.teitem_posted, 85 | teitem_curr_id=new.teitem_curr_id, 86 | teitem_uom_id=new.teitem_uom_id, 87 | teitem_invcitem_id=new.teitem_invcitem_id, 88 | teitem_vodist_id=new.teitem_vodist_id, 89 | teitem_postedvalue=new.teitem_postedvalue, 90 | teitem_empcost=new.teitem_empcost 91 | where teitem_id = old.teitem_id; 92 | 93 | create or replace rule "_DELETE" as on delete to xt.teiteminfo do instead 94 | 95 | delete from te.teitem where teitem_id=old.teitem_id; -------------------------------------------------------------------------------- /source/bi_open/client/widgets/bi_chart_nopicker.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, nv:true, d3:true, dimple:true console:true */ 5 | 6 | (function () { 7 | 8 | /** 9 | Implementation of BiChart Responsible for: 10 | - enyo components 11 | - filter management 12 | - requesting update of query templates based on pickers 13 | - creating chart area 14 | */ 15 | enyo.kind( 16 | /** @lends XV.BiChartMeasure# */{ 17 | name: "XV.BiChartNoPicker", 18 | kind: "XV.BiChart", 19 | published: { 20 | chartType: "barChart", 21 | chartTag: "svg", 22 | maxHeight: 0, 23 | maxWidth: 0, 24 | measures: [], 25 | // queryParms: 26 | time: "", 27 | where: [], 28 | year: "current", 29 | month: "current", 30 | // May want to override these in the implementation 31 | parameterWidget: "XV.SalesChartParameters" 32 | }, 33 | handlers: { 34 | onParameterChange: "parameterDidChange" 35 | }, 36 | 37 | /** 38 | Kickoff fetch of collections. 39 | */ 40 | create: function () { 41 | var that = this, 42 | model = this.getModel(); 43 | this.inherited(arguments); 44 | 45 | // Set last filter uuid. This will drive fetchCollection if a filter is defined 46 | this.setLastFilter(); 47 | 48 | // Fill in the queryTemplate and ask the Collection to get data. 49 | this.updateQueries(); 50 | this.fetchCollection(); 51 | }, 52 | /** 53 | Set chart component widths and heights using max sizes from dashboard - up to chart implementor. 54 | */ 55 | setComponentSizes: function (maxHeight, maxWidth) { 56 | var height = Number(maxHeight) - 20, 57 | width = Number(maxWidth) - 20; 58 | this.setMaxHeight(maxHeight); // for filterTapped to use later 59 | this.setMaxWidth(maxWidth); // for filterTapped to use later 60 | this.$.chartGroup.setStyle("width:" + width + "px;"); 61 | this.$.chartHeader.setStyle("width:" + width + "px;"); 62 | this.setStyle("width:" + width + "px;height:" + height + "px;"); // class selectable-chart 63 | this.$.chartWrapper.setStyle("width:" + width + "px;height:" + (height - 32) + "px;"); 64 | this.$.chartTitle.setStyle("width:" + width + "px;height:28px;"); 65 | this.$.chart.setStyle("width:" + width + "px;height:" + (height - 16) + "px;"); 66 | }, 67 | /** 68 | * Destroy and re-plot the chart area when the data changes. 69 | */ 70 | processedDataChanged: function () { 71 | this.createChartComponent(); 72 | this.plot(this.getChartType()); 73 | }, 74 | 75 | }); 76 | 77 | }()); 78 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/trigger_functions/teitem.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.triggerteitem() RETURNS "trigger" AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | _r RECORD; 6 | _status CHAR(1) := 'O'; 7 | 8 | BEGIN 9 | -- Validate whether we can take this action 10 | IF (TG_OP = 'UPDATE') THEN 11 | IF ((OLD.teitem_type != NEW.teitem_type) 12 | OR (OLD.teitem_workdate != NEW.teitem_workdate) 13 | OR (OLD.teitem_cust_id != NEW.teitem_cust_id) 14 | OR (OLD.teitem_po != NEW.teitem_po) 15 | OR (OLD.teitem_item_id != NEW.teitem_item_id) 16 | OR (OLD.teitem_qty != NEW.teitem_qty) 17 | OR (OLD.teitem_rate != NEW.teitem_rate) 18 | OR (OLD.teitem_total != NEW.teitem_total) 19 | OR (OLD.teitem_billable != NEW.teitem_billable) 20 | OR (OLD.teitem_prepaid != NEW.teitem_prepaid) 21 | OR (OLD.teitem_notes != NEW.teitem_notes)) THEN 22 | 23 | SELECT tehead_status INTO _status FROM te.tehead WHERE tehead_id=NEW.teitem_tehead_id; 24 | END IF; 25 | ELSIF (TG_OP = 'INSERT') THEN 26 | SELECT tehead_status INTO _status FROM te.tehead WHERE tehead_id=NEW.teitem_tehead_id; 27 | ELSE -- Must be delete 28 | SELECT tehead_status INTO _status FROM te.tehead WHERE tehead_id=OLD.teitem_tehead_id; 29 | END IF; 30 | 31 | IF (_status != 'O') THEN 32 | RAISE EXCEPTION 'Time and Expense Sheets may only be edited or deleted when the status is Open'; 33 | END IF; 34 | 35 | _status := 'C'; 36 | 37 | -- Update header status, default is to close if all processing complete 38 | IF (TG_OP = 'UPDATE') THEN 39 | IF ((COALESCE(OLD.teitem_invcitem_id,-1) != COALESCE(NEW.teitem_invcitem_id,-1)) 40 | OR (COALESCE(OLD.teitem_vodist_id,-1) != COALESCE(NEW.teitem_vodist_id,-1)) 41 | OR (OLD.teitem_posted != NEW.teitem_posted)) THEN 42 | 43 | SELECT 44 | te.sheetstate(NEW.teitem_tehead_id, 'I') AS invoiced, 45 | te.sheetstate(NEW.teitem_tehead_id, 'V') AS vouchered, 46 | te.sheetstate(NEW.teitem_tehead_id, 'P') AS posted 47 | INTO _r; 48 | 49 | IF (_r.invoiced = 0 OR _r.vouchered = 0 OR _r.posted = 0) THEN 50 | _status := 'A'; -- Something is still open, so approved 51 | END IF; 52 | 53 | UPDATE te.tehead SET tehead_status = _status WHERE (tehead_id=NEW.teitem_tehead_id); 54 | END IF; 55 | END IF; 56 | 57 | -- Update header with last use info 58 | IF (TG_OP = 'DELETE') THEN 59 | UPDATE te.tehead SET 60 | tehead_lastupdated=('now'::text)::timestamp(6) with time zone 61 | WHERE (tehead_id=OLD.teitem_tehead_id); 62 | ELSE 63 | UPDATE te.tehead SET 64 | tehead_lastupdated=('now'::text)::timestamp(6) with time zone, 65 | tehead_username=getEffectiveXtUser() 66 | WHERE (tehead_id=NEW.teitem_tehead_id); 67 | END IF; 68 | 69 | RETURN NEW; 70 | END; 71 | $$ LANGUAGE 'plpgsql'; 72 | -------------------------------------------------------------------------------- /source/time_expense/client/models/item.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true, Backbone:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.timeExpense.initItemModels = function () { 10 | 11 | var _proto = XM.Item.prototype, 12 | _bindEvents = _proto.bindEvents, 13 | _statusDidChange = _proto.statusDidChange, 14 | _itemTypeDidChange = function () { 15 | var itemType = this.get("itemType"), 16 | readOnly = false; 17 | if (itemType !== XM.Item.REFERENCE) { 18 | readOnly = true; 19 | this.set("projectExpenseMethod", null); 20 | } 21 | this.setReadOnly("projectExpenseMethod", readOnly); 22 | this.methodDidChange(); 23 | }; 24 | 25 | XM.Item.EXPENSE_BY_CATEGORY = "E"; 26 | XM.Item.EXPENSE_BY_ACCOUNT = "A"; 27 | 28 | XM.Item = XM.Item.extend({ 29 | bindEvents: function () { 30 | _bindEvents.apply(this, arguments); 31 | this.on("change:projectExpenseMethod", this.methodDidChange); 32 | this.on("change:itemType", _itemTypeDidChange, this); 33 | }, 34 | 35 | methodDidChange: function () { 36 | var K = XM.Item, 37 | method = this.get("projectExpenseMethod"), 38 | editCategory = false, 39 | editAccount = false, 40 | unset = ["projectExpenseCategory", "projectExpenseLedgerAccount"]; 41 | if (method === K.EXPENSE_BY_CATEGORY) { 42 | editCategory = true; 43 | unset.shift(); 44 | } else if (method === K.EXPENSE_BY_ACCOUNT) { 45 | editAccount = true; 46 | unset.pop(); 47 | } 48 | this.setReadOnly("projectExpenseCategory", !editCategory); 49 | this.setReadOnly("projectExpenseLedgerAccount", !editAccount); 50 | while (unset.length) { this.unset(unset.pop()); } 51 | }, 52 | 53 | statusDidChange: function () { 54 | _statusDidChange.apply(this, arguments); 55 | var K = XM.Model, 56 | status = this.getStatus(); 57 | if (status === K.READY_NEW || status === K.READY_CLEAN) { 58 | _itemTypeDidChange.apply(this); 59 | } 60 | } 61 | 62 | }); 63 | 64 | // Static Model 65 | var i, 66 | K = XM.Item, 67 | itemExpenseOptionJson = [ 68 | { id: K.EXPENSE_BY_CATEGORY, name: "_byCategory".loc() }, 69 | { id: K.EXPENSE_BY_ACCOUNT, name: "_byAccount".loc() } 70 | ]; 71 | XM.ItemExpenseOption = Backbone.Model.extend({ 72 | }); 73 | XM.ItemExpenseOptionCollection = Backbone.Collection.extend({ 74 | model: XM.ItemExpenseOption 75 | }); 76 | XM.itemExpenseOptions = new XM.ItemExpenseOptionCollection(); 77 | for (i = 0; i < itemExpenseOptionJson.length; i++) { 78 | var itemExpenseOption = new XM.ItemExpenseOption(itemExpenseOptionJson[i]); 79 | XM.itemExpenseOptions.add(itemExpenseOption); 80 | } 81 | 82 | }; 83 | 84 | }()); 85 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/postsheet.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.postsheet(integer, text, text) RETURNS integer AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | pTeheadId ALIAS FOR $1; 6 | pPhrase1 ALIAS FOR $2; 7 | pPhrase2 ALIAS FOR $3; 8 | _r record; 9 | _notes TEXT; 10 | _value NUMERIC; 11 | _olaccntid INTEGER; 12 | _expaccntid INTEGER; 13 | _count INTEGER; 14 | 15 | BEGIN 16 | -- Validate: No posting for contractors 17 | IF (SELECT (count(teemp_id) > 0) 18 | FROM te.tehead 19 | JOIN te.teemp ON (tehead_emp_id=teemp_emp_id) 20 | WHERE ((tehead_id=pTeheadId) 21 | AND (teemp_contractor))) THEN 22 | RAISE EXCEPTION 'Time and Expense Sheets can not be posted for contractors. Voucher instead.'; 23 | END IF; 24 | 25 | -- Get labor and overhead account 26 | SELECT accnt_id INTO _olaccntid 27 | FROM accnt 28 | WHERE (accnt_id=fetchmetricvalue('PrjLaborAndOverhead')); 29 | 30 | GET DIAGNOSTICS _count = ROW_COUNT; 31 | IF (_count = 0) THEN 32 | RAISE EXCEPTION 'No valid Project Labor and Overhead Account Defined'; 33 | END IF; 34 | 35 | -- Get applicable time sheets 36 | FOR _r IN 37 | SELECT tehead_number, 38 | teitem_id, teitem_linenumber, teitem_type, teitem_notes, 39 | item_descrip1, teitem_qty, 40 | teexp_expcat_id, teexp_accnt_id, 41 | emp_code, emp_wage, emp_wage_period, 42 | prj_id, prj_number 43 | FROM te.tehead 44 | JOIN te.teitem ON (teitem_tehead_id=tehead_id) 45 | JOIN item ON (teitem_item_id=item_id) 46 | JOIN te.teexp ON (teitem_item_id=teexp_id) 47 | JOIN emp ON (tehead_emp_id=emp_id) 48 | JOIN prjtask ON (prjtask_id=teitem_prjtask_id) 49 | JOIN prj ON (prj_id=prjtask_prj_id) 50 | WHERE ((tehead_id = pTeheadId) 51 | AND (NOT teitem_posted) 52 | AND (teitem_vodist_id IS NULL) 53 | AND (teitem_type = 'T')) 54 | 55 | LOOP 56 | -- Determine value 57 | _value := te.calcRate(_r.emp_wage, _r.emp_wage_period) * _r.teitem_qty; 58 | 59 | -- Determine G/L account to post to 60 | IF (_r.teexp_accnt_id > 1) THEN 61 | _expaccntid := getPrjAccntId(_r.prj_id, _r.teexp_accnt_id); 62 | ELSE 63 | SELECT getPrjAccntId(_r.prj_id, expcat_exp_accnt_id) INTO _expaccntid 64 | FROM expcat 65 | WHERE (expcat_id=_r.teexp_expcat_id); 66 | END IF; 67 | 68 | -- Execute the posting 69 | _notes := (pPhrase1 || _r.item_descrip1 || '/' || _r.emp_code || pPhrase2 || ' ' || _r.prj_number); 70 | PERFORM insertGLTransaction( 'T/E', 'TE', _r.tehead_number, _notes, 71 | _olaccntid, _expaccntid, -1, 72 | _value, current_date ); 73 | 74 | -- Update the time sheet item 75 | UPDATE te.teitem SET 76 | teitem_posted = true, 77 | teitem_postedvalue = teitem_postedvalue + _value 78 | WHERE (teitem_id=_r.teitem_id); 79 | 80 | END LOOP; 81 | 82 | RETURN 1; 83 | END; 84 | $$ LANGUAGE 'plpgsql' VOLATILE; 85 | -------------------------------------------------------------------------------- /test/time_expense/specs/employee.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XV:true, XT:true, _:true, console:true, XM:true, Backbone:true, require:true, assert:true, 5 | setTimeout:true, before:true, clearTimeout:true, exports:true, it:true, describe:true, beforeEach:true */ 6 | 7 | (function () { 8 | "use strict"; 9 | 10 | /** 11 | Employees are people who work for your company. 12 | An Employee may or may not be an xTuple ERP system user 13 | @class 14 | @alias Employee 15 | */ 16 | var spec = { 17 | skipSmoke: true, 18 | skipCrud: true, 19 | recordType: "XM.Employee", 20 | collectionType: null, 21 | cacheName: null, 22 | listKind: "XV.EmployeeList", 23 | instanceOf: "XM.Document", 24 | /** 25 | @member - 26 | @memberof Employee 27 | @description Employees are lockable. 28 | */ 29 | isLockable: true, 30 | /** 31 | @member - 32 | @memberof Employee 33 | @description The ID attribute is "code", which will be automatically uppercased. 34 | */ 35 | idAttribute: "code", 36 | enforceUpperKey: true, 37 | attributes: ["id", "code", "number", "name", "isActive", "contact", "startDate", "site", 38 | "owner", "manager", "department", "shift", "wageType", "wage", "wageCurrency", 39 | "wagePeriod", "billingRate", "billingPeriod", "notes", "comments", "characteristics", 40 | "groups", "isContractor"], 41 | /** 42 | @member - 43 | @memberof Employee 44 | @description Used in the Project module 45 | */ 46 | extensions: ["project"], 47 | relevantPrivilegeModule: "setup", 48 | /** 49 | @member - 50 | @memberof Employee 51 | @description Employees can be read by users with "ViewEmployees" privilege and can be 52 | created, updated, or deleted by users with the "MaintainEmployees" privilege. 53 | */ 54 | privileges: { 55 | createUpdateDelete: "MaintainEmployees", 56 | read: "ViewEmployees" 57 | } 58 | }; 59 | var additionalTests = function () { 60 | /** 61 | @member - 62 | @memberof Employee 63 | @description Employee screen contains a Comment panel to add comments to Employee record 64 | */ 65 | it.skip("Employee screen contains a Comment panel to add comments to Employee record", 66 | function () { 67 | }); 68 | /** 69 | @member - 70 | @memberof Employee 71 | @description Employees could be attached/detached to Groups from the Employee screen 72 | */ 73 | it.skip("Employees could be attached/detached to Groups from the Employee screen", 74 | function () { 75 | }); 76 | /** 77 | @member - 78 | @memberof Employee 79 | @description Employees with history and employees attached to groups cannot be deleted 80 | */ 81 | it.skip("Employees with history and employees attached to groups cannot be deleted", 82 | function () { 83 | }); 84 | }; 85 | exports.spec = spec; 86 | exports.additionalTests = additionalTests; 87 | }()); 88 | 89 | -------------------------------------------------------------------------------- /source/bi_open/client/postbooks.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XV:true, XM:true, enyo:true, console:true */ 5 | 6 | XT.extensions.bi_open.initPostbooks = function () { 7 | 8 | var panelsCrmDash = [ 9 | {name: "crmBiDashboard", kind: "XV.CrmBiDashboard"}, 10 | ], 11 | dashboardModule = { 12 | name: "welcomeDashboard", 13 | hasSubmenu: false, 14 | label: "_dashboard".loc(), 15 | panels: [ 16 | {name: "welcomeDashboard", kind: "XV.WelcomeDashboard"} 17 | ] 18 | }, 19 | /* 20 | * chartActions will become newActions if they have the specified privileges 21 | */ 22 | chartActions = [ 23 | /* 24 | * Opportunity charts 25 | */ 26 | {name: "opportunitiesTrailing", label: "_opportunitiesTrailing".loc(), item: "XV.Period12OpportunitiesTimeSeriesChart", privileges: ["ViewAllOpportunities"]}, 27 | {name: "opportunitiesBookingsTrailing", label: "_opportunitiesBookingsTrailing".loc(), item: "XV.Period12OpportunitiesBookingsTimeSeriesChart", privileges: ["ViewAllOpportunities", "ViewSalesOrders"]}, 28 | {name: "opportunitiesActiveNext", label: "_opportunitiesActiveNext".loc(), item: "XV.Next12OpportunitiesActiveTimeSeriesChart", privileges: ["ViewAllOpportunities"]}, 29 | {name: "opportunityForecastTrailing", label: "_opportunityForecastTrailing".loc(), item: "XV.Period12OpportunityForecastTimeSeriesChart", privileges: ["ViewAllOpportunities"]}, 30 | {name: "opportunitytl", label: "_toplistTrailingOpportunity".loc(), item: "XV.Period12OpportunityToplistChart", privileges: ["ViewAllOpportunities"]}, 31 | {name: "opportunitytal", label: "_toplistTrailingOpportunityActive".loc(), item: "XV.Period12OpportunityActiveToplistChart", privileges: ["ViewAllOpportunities"]}, 32 | /* 33 | * Quote charts 34 | */ 35 | {name: "quoteTrailing", label: "_quotesTrailing".loc(), item: "XV.Period12QuotesTimeSeriesChart", privileges: ["ViewQuotes"]}, 36 | {name: "quoteActiveTrailing", label: "_quotesActiveTrailing".loc(), item: "XV.Period12QuotesActiveTimeSeriesChart", privileges: ["ViewQuotes"]}, 37 | {name: "quotetl", label: "_toplistTrailingQuote".loc(), item: "XV.Period12QuoteToplistChart", privileges: ["ViewQuotes"]}, 38 | {name: "quoteActivetl", label: "_toplistTrailingQuoteActive".loc(), item: "XV.Period12QuoteActiveToplistChart", privileges: ["ViewQuotes"]}, 39 | /* 40 | * Sales Pipeline charts 41 | */ 42 | {name: "opportunityFunnel", label: "_opportunitiesFunnel".loc(), item: "XV.FunnelOpportunitiesChart", privileges: ["ViewAllOpportunities"]}, 43 | {name: "salesVelocity", label: "_salesVelocity".loc(), item: "XV.Period12SumSalesVelocityChart", privileges: ["ViewAllOpportunities"]}, 44 | ]; 45 | 46 | XT.app.$.postbooks.appendPanels("crm", panelsCrmDash, true); 47 | XT.app.$.postbooks.insertModule(dashboardModule, 0); 48 | // Add chart actions to global XT.chartActions that we set up in core.js 49 | XT.chartActions.push.apply(XT.chartActions, chartActions); 50 | 51 | }; -------------------------------------------------------------------------------- /source/time_expense/database/orm/models/project.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "time_expense", 4 | "nameSpace": "XM", 5 | "type": "WorksheetProjectRelation", 6 | "table": "xt.prjinfo", 7 | "isRest": true, 8 | "comment": "Worksheet Project Relation Map", 9 | "privileges": { 10 | "all": { 11 | "create": false, 12 | "read": "ViewAllProjects MaintainAllProjects", 13 | "update": false, 14 | "delete": false 15 | }, 16 | "personal": { 17 | "create": false, 18 | "read": true, 19 | "update": false, 20 | "delete": false, 21 | "properties": [ 22 | "owner.username", 23 | "assignedTo.username" 24 | ] 25 | } 26 | }, 27 | "properties": [ 28 | { 29 | "name": "id", 30 | "attr": { 31 | "type": "Number", 32 | "column": "prj_id", 33 | "isPrimaryKey": true 34 | } 35 | }, 36 | { 37 | "name": "number", 38 | "attr": { 39 | "type": "String", 40 | "column": "prj_number", 41 | "isNaturalKey":true 42 | } 43 | }, 44 | { 45 | "name": "name", 46 | "attr": { 47 | "type": "String", 48 | "column": "prj_name" 49 | } 50 | }, 51 | { 52 | "name": "status", 53 | "attr": { 54 | "type": "String", 55 | "column": "prj_status" 56 | } 57 | }, 58 | { 59 | "name": "dueDate", 60 | "attr": { 61 | "type": "DueDate", 62 | "column": "prj_due_date" 63 | } 64 | }, 65 | { 66 | "name": "assignedTo", 67 | "toOne": { 68 | "isNested": true, 69 | "type": "UserAccountRelation", 70 | "column": "prj_username", 71 | "inverse": "username" 72 | } 73 | }, 74 | { 75 | "name": "owner", 76 | "toOne": { 77 | "isNested": true, 78 | "type": "UserAccountRelation", 79 | "column": "prj_owner_username", 80 | "inverse": "username" 81 | } 82 | }, 83 | { 84 | "name": "account", 85 | "attr": { 86 | "type": "String", 87 | "column": "crmacct_number" 88 | } 89 | }, 90 | { 91 | "name": "contact", 92 | "attr": { 93 | "type": "String", 94 | "column": "cntct_number" 95 | } 96 | }, 97 | { 98 | "name": "tasks", 99 | "toMany": { 100 | "isNested": true, 101 | "type": "ProjectTaskRelation", 102 | "column": "prj_id", 103 | "inverse": "project" 104 | } 105 | } 106 | ], 107 | "extensions": [ 108 | { 109 | "table": "xt.teprjinfo", 110 | "relations": [ 111 | { 112 | "column": "teprj_prj_id", 113 | "inverse": "id" 114 | } 115 | ], 116 | "properties": [ 117 | { 118 | "name": "customer", 119 | "toOne": { 120 | "isNested": true, 121 | "type": "CustomerRelation", 122 | "column": "teprj_cust_id" 123 | } 124 | } 125 | ] 126 | } 127 | ], 128 | "isSystem": true 129 | } 130 | ] -------------------------------------------------------------------------------- /source/time_expense/client/views/list_relations.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XV:true, enyo:true, Globalize:true */ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initListRelations = function () { 9 | 10 | // .......................................................... 11 | // WORKSHEET 12 | // 13 | 14 | enyo.kind({ 15 | name: "XV.WorksheetTimeListRelations", 16 | kind: "XV.ListRelations", 17 | orderBy: [ 18 | {attribute: 'lineNumber' } 19 | ], 20 | parentKey: "worksheet", 21 | components: [ 22 | {kind: "XV.ListItem", components: [ 23 | {kind: "FittableColumns", components: [ 24 | {kind: "XV.ListColumn", classes: "first", components: [ 25 | {kind: "FittableColumns", components: [ 26 | {kind: "XV.ListAttr", attr: "lineNumber", classes: "bold"}, 27 | {kind: "XV.ListAttr", formatter: "formatProjectTask"}, 28 | {kind: "XV.ListAttr", attr: "workDate", fit: true, 29 | classes: "right"} 30 | ]}, 31 | {kind: "FittableColumns", components: [ 32 | {kind: "XV.ListAttr", attr: "item.number", 33 | style: "text-indent: 18px;"}, 34 | {kind: "XV.ListAttr", attr: "hours", 35 | classes: "right", formatter: "formatHours"} 36 | ]} 37 | ]} 38 | ]} 39 | ]} 40 | ], 41 | formatHours: function (value, view, model) { 42 | view.addRemoveClass("error", value < 0); 43 | return Globalize.format(value, "n" + 2) + " " + "_hrs".loc(); 44 | }, 45 | formatProjectTask: function (value, view, model) { 46 | var projectNumber = model.getValue('task.project.number'), 47 | taskNumber = model.getValue('task.number'); 48 | return projectNumber + ' - ' + taskNumber; 49 | } 50 | }); 51 | 52 | enyo.kind({ 53 | name: "XV.WorksheetExpenseListRelations", 54 | kind: "XV.WorksheetTimeListRelations", 55 | orderBy: [ 56 | {attribute: 'lineNumber' } 57 | ], 58 | parentKey: "worksheet", 59 | components: [ 60 | {kind: "XV.ListItem", components: [ 61 | {kind: "FittableColumns", components: [ 62 | {kind: "XV.ListColumn", classes: "first", components: [ 63 | {kind: "FittableColumns", components: [ 64 | {kind: "XV.ListAttr", attr: "lineNumber", classes: "bold"}, 65 | {kind: "XV.ListAttr", formatter: "formatProjectTask"}, 66 | {kind: "XV.ListAttr", attr: "workDate", fit: true, 67 | classes: "right"} 68 | ]}, 69 | {kind: "FittableColumns", components: [ 70 | {kind: "XV.ListAttr", attr: "item.number", 71 | style: "text-indent: 18px;"}, 72 | {kind: "XV.ListAttr", attr: "total", 73 | classes: "right", formatter: "formatMoney"} 74 | ]} 75 | ]} 76 | ]} 77 | ]} 78 | ], 79 | formatMoney: function (value, view, model) { 80 | view.addRemoveClass("error", value < 0); 81 | return Globalize.format(value, "n" + 2); 82 | } 83 | }); 84 | 85 | }; 86 | 87 | }()); 88 | -------------------------------------------------------------------------------- /source/bi_open/node-datasource/routes/analysis.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, strict:true, trailing:true, white:true */ 3 | /*global X:true, _:true, SYS:true */ 4 | 5 | (function () { 6 | "use strict"; 7 | 8 | var utils = require('../../../../../xtuple/node-datasource/oauth2/utils'); 9 | 10 | /** 11 | Generates a JSON Web Token (JWT) to be appended to the 12 | BI Server URL so that it may authenticate the current user. 13 | */ 14 | exports.analysis = function (req, res) { 15 | var jwt, 16 | privKey = "", 17 | claimSet = {}, 18 | header = {}, 19 | reportUrl = req.query.reportUrl, 20 | username = req.session.passport.user.username, 21 | biServerHost = X.options.biServer.bihost || "localhost", 22 | biServerPortHttps = X.options.biServer.httpsport || "8443", 23 | biServerUrl = "https://" + biServerHost + ":" + biServerPortHttps + "/pentaho/", 24 | today = new Date(), 25 | expires = new Date(today.getTime() + (10 * 60 * 1000)), // 10 minutes from now 26 | datasource = "https://" + req.headers.host + "/", 27 | database = req.session.passport.user.organization, 28 | scope = datasource + database + "/auth/" + database, 29 | audience = datasource + database + "/oauth/token", 30 | superuser = X.options.databaseServer.user, 31 | tenant = X.options.biServer.tenantname || "default", 32 | biKeyFile = X.options.biServer.restkeyfile || ""; 33 | 34 | // get private key from path in config 35 | privKey = X.fs.readFileSync(biKeyFile); 36 | 37 | // create header for JWT 38 | header = { 39 | "alg": "RS256", 40 | "type": "JWT" 41 | }; 42 | 43 | // create claimSet for JWT 44 | claimSet = { 45 | "prn": username, // username 46 | "scope": scope, 47 | "aud": audience, 48 | "org": database, 49 | "superuser": superuser, // database user 50 | "datasource": datasource, // rest api url 51 | "exp": Math.round(expires.getTime() / 1000), // expiration date in millis 52 | "iat": Math.round(today.getTime() / 1000), // created date in millis 53 | "tenant": tenant || "default" // unique tenant id 54 | }; 55 | 56 | // encode and sign JWT with private key 57 | jwt = encodeJWT(JSON.stringify(header), JSON.stringify(claimSet), privKey); 58 | // send newly formed BI url back to the client 59 | res.send(biServerUrl + reportUrl + "&assertion=" + jwt.jwt); 60 | }; 61 | 62 | var encodeJWT = function (header, claimSet, key) { 63 | var encodeHeader, 64 | encodeClaimSet, 65 | signer, 66 | signature, 67 | data, 68 | jwt; 69 | 70 | if (!key) { 71 | X.log("No private key"); 72 | } 73 | 74 | // if there is a problem encoding/signing the JWT, then return invalid 75 | try { 76 | encodeHeader = utils.base64urlEncode(JSON.stringify(JSON.parse(header))); 77 | encodeClaimSet = utils.base64urlEncode(JSON.stringify(JSON.parse(claimSet))); 78 | data = encodeHeader + "." + encodeClaimSet; 79 | 80 | signer = X.crypto.createSign("RSA-SHA256"); 81 | signer.update(data); 82 | signature = utils.base64urlEscape(signer.sign(key, "base64")); 83 | jwt = { 84 | jwt: data + "." + signature 85 | }; 86 | 87 | } catch (error) { 88 | jwt = { 89 | jwt: "invalid" 90 | }; 91 | X.log("Invalid JWT"); 92 | } 93 | 94 | return jwt; 95 | }; 96 | 97 | }()); 98 | -------------------------------------------------------------------------------- /source/bi_open/node-datasource/routes/olapdata.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, strict:true, trailing:true, white:true */ 3 | /*global X:true, XT:true */ 4 | 5 | (function () { 6 | "use strict"; 7 | var utils = require('../../../../../xtuple/node-datasource/oauth2/utils'); 8 | X.xmla = require('xmla4js/src/Xmla.js'); 9 | 10 | require("../olapcatalog/olapcatalog"); 11 | require("../olapcatalog/olapsource"); 12 | 13 | exports.queryOlapCatalog = function (req, res) { 14 | 15 | X.debug("\nolapdata query: " + JSON.stringify(req.query.mdx)); 16 | var query = req.query.mdx, 17 | // Format xmla response as json and return 18 | queryCallback = function (xmlaResponse) { 19 | var obj = xmlaResponse.fetchAllAsObject(); 20 | obj = {data : obj}; 21 | X.debug("\nolapdata query result: " + JSON.stringify(obj)); 22 | res.writeHead(200, { 'Content-Type': 'application/json' }); 23 | res.write(JSON.stringify(obj)); 24 | res.end(); 25 | }, 26 | 27 | jwt, 28 | privKey = "", 29 | claimSet = {}, 30 | header = {}, 31 | username = req.session.passport.user.username, 32 | today = new Date(), 33 | expires = new Date(today.getTime() + (10 * 60 * 1000)), // 10 minutes from now 34 | datasource = "https://" + req.headers.host + "/", 35 | database = req.session.passport.user.organization, 36 | scope = datasource + database + "/auth/" + database, 37 | audience = datasource + database + "/oauth/token", 38 | superuser = X.options.databaseServer.user, 39 | tenant = X.options.biServer.tenantname || "default", 40 | biKeyFile = X.options.biServer.restkeyfile || ""; 41 | 42 | // get private key from path in config 43 | privKey = X.fs.readFileSync(biKeyFile); 44 | 45 | // create header for JWT 46 | header = { 47 | "alg": "RS256", 48 | "type": "JWT" 49 | }; 50 | 51 | // create claimSet for JWT 52 | claimSet = { 53 | "prn": username, // username 54 | "scope": scope, 55 | "aud": audience, 56 | "org": database, 57 | "superuser": superuser, // database user 58 | "datasource": datasource, // rest api url 59 | "exp": Math.round(expires.getTime() / 1000), // expiration date in millis 60 | "iat": Math.round(today.getTime() / 1000), // created date in millis 61 | "tenant": tenant || "default" // unique tenant id 62 | }; 63 | 64 | // encode and sign JWT with private key 65 | jwt = encodeJWT(JSON.stringify(header), JSON.stringify(claimSet), privKey); 66 | 67 | X.olapSource.query(query, jwt, queryCallback); 68 | }; 69 | 70 | var encodeJWT = function (header, claimSet, key) { 71 | var encodeHeader, 72 | encodeClaimSet, 73 | signer, 74 | signature, 75 | data, 76 | jwt; 77 | 78 | if (!key) { 79 | X.log("No private key"); 80 | } 81 | 82 | // if there is a problem encoding/signing the JWT, then return invalid 83 | try { 84 | encodeHeader = utils.base64urlEncode(JSON.stringify(JSON.parse(header))); 85 | encodeClaimSet = utils.base64urlEncode(JSON.stringify(JSON.parse(claimSet))); 86 | data = encodeHeader + "." + encodeClaimSet; 87 | 88 | signer = X.crypto.createSign("RSA-SHA256"); 89 | signer.update(data); 90 | signature = utils.base64urlEscape(signer.sign(key, "base64")); 91 | jwt = { 92 | jwt: data + "." + signature 93 | }; 94 | 95 | } catch (error) { 96 | jwt = { 97 | jwt: "invalid" 98 | }; 99 | X.log("Invalid JWT"); 100 | } 101 | 102 | return jwt; 103 | }; 104 | }()); 105 | 106 | -------------------------------------------------------------------------------- /source/bi_open/client/widgets/parameter.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true, strict:false*/ 4 | /*global XT:true, XM:true, _:true, enyo:true, Globalize:true*/ 5 | 6 | (function () { 7 | 8 | enyo.kind({ 9 | name: "XV.ChartParameterWidget", 10 | kind: "XV.ParameterWidget", 11 | /** 12 | * We need our own ParameterWidget so we can use the the last filter selected for this 13 | * instance of the chart. We can't use filters by type as there can be multiple charts 14 | * of the same type. The chart is responsible calling setLastFilterUuid. 15 | */ 16 | populateFromUserPref: function () { 17 | // override and don't populate from user preferences 18 | }, 19 | setLastFilterUuid: function (value) { 20 | this.$.filterForm.$.itemPicker.setValue(value); 21 | } 22 | }); 23 | 24 | enyo.kind({ 25 | name: "XV.SalesChartParameters", 26 | kind: "XV.ChartParameterWidget", 27 | classes: "chart-filters", 28 | components: [ 29 | {name: "salesRep", attr: "salesRep", label: "_salesRep".loc(), defaultKind: "XV.SalesRepPicker"}, 30 | {name: "customer", label: "_customer".loc(), attr: "customer", defaultKind: "XV.CustomerWidget"}, 31 | {kind: "onyx.GroupboxHeader", content: "_item".loc()}, 32 | {name: "itemWidget", label: "_item".loc(), attr: "item", defaultKind: "XV.ItemWidget"}, 33 | {name: "itemType", 34 | label: "_type".loc(), 35 | attr: "itemType", 36 | defaultKind: "XV.ItemTypePicker", 37 | getParameter: function () { 38 | // use static model itemTypes to get the string for the item type code 39 | var value = this.getValue(), 40 | param; 41 | if (value) { 42 | param = {attribute: "itemType", 43 | value: {id: XM.itemTypes.get(value).attributes.name} 44 | }; 45 | } 46 | return param; 47 | } 48 | }, 49 | {name: "category", label: "_category".loc(), attr: "productCategory", defaultKind: "XV.ProductCategoryPicker"}, 50 | {name: "classCode", label: "_class".loc(), attr: "classCode", defaultKind: "XV.ClassCodePicker"}, 51 | {kind: "onyx.GroupboxHeader", content: "_endPeriod".loc()}, 52 | {name: "year", label: "_year".loc(), defaultKind: "XV.EndYearPicker", attr: "year"}, 53 | {name: "month", label: "_month".loc(), defaultKind: "XV.EndMonthPicker", attr: "month"}, 54 | /* 55 | {kind: "XV.CountryPicker", attr: "DefaultAddressCountry", label: "_default".loc(), idAttribute: "name"}, 56 | {name: "stage", label: "_stage".loc(), attr: "opportunityStage", defaultKind: "XV.OpportunityStagePicker"}, 57 | {kind: "XV.OpportunityTypePicker", attr: "opportunityType", label: "_type".loc()} 58 | */ 59 | 60 | ] 61 | }); 62 | 63 | enyo.kind({ 64 | name: "XV.OpportunityChartParameters", 65 | kind: "XV.ChartParameterWidget", 66 | classes: "chart-filters", 67 | components: [ 68 | {name: "account", label: "_account".loc(), attr: "account", defaultKind: "XV.AccountWidget"}, 69 | {name: "user", label: "_user".loc(), attr: "user", defaultKind: "XV.UserAccountWidget"}, 70 | {kind: "onyx.GroupboxHeader", content: "_endPeriod".loc()}, 71 | {name: "year", label: "_year".loc(), defaultKind: "XV.EndYearPicker", attr: "year"}, 72 | {name: "month", label: "_month".loc(), defaultKind: "XV.EndMonthPicker", attr: "month"}, 73 | 74 | ] 75 | }); 76 | 77 | enyo.kind({ 78 | name: "XV.TimeChartParameters", 79 | kind: "XV.ChartParameterWidget", 80 | classes: "chart-filters", 81 | components: [ 82 | {kind: "onyx.GroupboxHeader", content: "_endPeriod".loc()}, 83 | {name: "year", label: "_year".loc(), defaultKind: "XV.EndYearPicker", attr: "year"}, 84 | {name: "month", label: "_month".loc(), defaultKind: "XV.EndMonthPicker", attr: "month"}, 85 | ] 86 | }); 87 | 88 | }()); 89 | -------------------------------------------------------------------------------- /source/bi_open/client/en/strings.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | strict:true, trailing:true, white:true */ 4 | /*global XT:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | var lang = XT.stringsFor("en_US", { 10 | "_actualOpportunities": "Actual Opportunities", 11 | "_account": "Account", 12 | "_active": "Active ", 13 | "_allOpportunities": "All Opportunities", 14 | "_allQuotes": "All Quotes", 15 | "_amount": "Amount", 16 | "_amountOpportunity": "Opportunity Amount", 17 | "_amountOpportunityForecast": "Opportunity Forecast", 18 | "_amountOpportunityForecastWeighted": "Opportunity Forecast Weighted", 19 | "_amountOpportunityWeighted": "Opportunity Weighted", 20 | "_amountQuote": "Quote Amount", 21 | "_amountQuoteDiscount": "Quote Discount", 22 | "_analysis": "Analysis", 23 | "_areaChart": "Area Chart", 24 | "_assignedOpportunities": "Assigned Opportunities", 25 | "_average": "Average", 26 | "_averageOpportunity": "Opportunity Average", 27 | "_averageOpportunityWeighted": "Opportunity Weighted Average", 28 | "_averageQuote": "Quote Average", 29 | "_barChart": "Bar Chart", 30 | "_billRegion": "Billing Region", 31 | "_bubbleChart": "Bubble Chart", 32 | "_chartFilters": "Chart Filters", 33 | "_chartType": "Chart Type", 34 | "_classCode": "Item Class", // really itemClass 35 | "_chooseDimensionMeasure": "Choose Dimension and Measure", 36 | "_chooseMeasure": "Choose Measure", 37 | "_convertedQuotes": "Converted Quotes", 38 | "_count": "Count", 39 | "_countForecastOpportunities": "Opportunity Forecast Count", 40 | "_countOpportunities": "Opportunity Count", 41 | "_countQuotes": "Quote Count", 42 | "_current": "Current", 43 | "_daysStartToActual": "Opportunity Start to Actual", 44 | "_daysStartToAssigned": "Opportunity Start to Assigned", 45 | "_daysStartToTarget": "Opportunity Start to Target", 46 | "_dimension": "Dimension", 47 | "_ending": " ending ", 48 | "_endPeriod": "Ending Period", 49 | "_item": "Item", 50 | "_itemType": "Item Type", 51 | "_lineChart": "Line Chart", 52 | "_maps": "Maps", 53 | "_measure": "Measure", 54 | "_measureName": "Measure Name", 55 | "_month": "Month", 56 | "_next3Months": "Next 3 Months", 57 | "_opportunity": "Opportunity", 58 | "_opportunitiesActiveNext": "Opportunities Next 6 months", 59 | "_opportunityForecastTrailing": "Opportunity Forecast", 60 | "_opportunitiesFunnel": "Opportunity Pipeline", 61 | "_opportunitiesTrailing": "Opportunities", 62 | "_opportunitiesBookingsTrailing": "Opportunity Conversion", 63 | "_percentForecastProbability": "Forecast Probability", 64 | "_percentOpportunityForecastProbability": "Opportunity Forecast Probability", 65 | "_percentProbabilityOpportunity": "Opportunity Probability", 66 | "_periodEnding": "Period Ending", 67 | "_previousYear": "Previous Year", 68 | "_productCategory": "Item Category", // really itemCategory 69 | "_quotesTrailing": "Quotes", 70 | "_quotesActiveTrailing": "Active Quotes", 71 | "_ratioConversion": "Opportunity Conversion Ratio", 72 | "_ratioConversionWeighted": "Opporunity Conversion Weighted Ratio", 73 | "_salesRep": "Sales Rep", 74 | "_salesVelocity": "Sales Velocity", 75 | "_staleAnalysisWarning": "Free trial demo analysis data will not be updated from your live changes.", 76 | "_targetedOpportunities": "Targeted Opportunities", 77 | "_toplistTrailing12": "Top List Trailing 12", 78 | "_toplistTrailingOpportunity": "Opportunities Top List", 79 | "_toplistTrailingOpportunityActive": "Active Opportunities Top List", 80 | "_toplistTrailingQuote": "Quotes Top List", 81 | "_toplistTrailingQuoteActive": "Active Quotes Top List", 82 | "_trailing12": " Trailing 12", 83 | "_wonOpportunities": "Opportunities Won", 84 | "_year": "Year" 85 | }); 86 | 87 | if (typeof exports !== 'undefined') { 88 | exports.language = lang; 89 | } 90 | }()); 91 | -------------------------------------------------------------------------------- /source/time_expense/client/views/list.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true, strict: false*/ 4 | /*global XT:true, XM:true, XV:true, enyo:true, Globalize:true*/ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initList = function () { 9 | 10 | // .......................................................... 11 | // WORKSHEET 12 | // 13 | 14 | enyo.kind({ 15 | name: "XV.WorksheetList", 16 | kind: "XV.List", 17 | label: "_worksheets".loc(), 18 | collection: "XM.WorksheetListItemCollection", 19 | multiSelect: true, 20 | handlers: { 21 | onSelect: "menuItemSelected" 22 | }, 23 | query: {orderBy: [ 24 | {attribute: 'number', numeric: true} 25 | ]}, 26 | actions: [ 27 | {name: "approve", privilege: "CanApprove", prerequisite: "canApprove", 28 | method: "doApprove", notify: false}, 29 | {name: "unapprove", privilege: "CanApprove", 30 | prerequisite: "canUnapprove", method: "doUnapprove", notify: false}, 31 | {name: "invoice", privilege: "allowInvoicing", 32 | prerequisite: "canInvoice", method: "doInvoice", notify: false}, 33 | {name: "voucher", privilege: "allowVouchering", 34 | prerequisite: "canVoucher", method: "doVoucher", notify: false}, 35 | {name: "post", privilege: "PostTimeSheets", prerequisite: "canPost", 36 | method: "doPost", notify: false}, 37 | {name: "close", privilege: "MaintainTimeExpense", 38 | prerequisite: "canClose", method: "doClose", 39 | notifyMessage: "_closeWorksheet?".loc()} 40 | ], 41 | parameterWidget: "XV.WorksheetListParameters", 42 | components: [ 43 | {kind: "XV.ListItem", components: [ 44 | {kind: "FittableColumns", components: [ 45 | {kind: "XV.ListColumn", classes: "first", components: [ 46 | {kind: "FittableColumns", components: [ 47 | {kind: "XV.ListAttr", attr: "number", isKey: true}, 48 | {kind: "XV.ListAttr", attr: "getWorksheetStatusString", 49 | formatter: "formatStatus"}, 50 | {kind: "XV.ListAttr", attr: "posted", formatter: "formatPosted"}, 51 | {kind: "XV.ListAttr", attr: "weekOf", fit: true, 52 | classes: "right"} 53 | ]}, 54 | {kind: "FittableColumns", components: [ 55 | {kind: "XV.ListAttr", attr: "employee.contact.name", 56 | placeholder: "_noContact".loc()}, 57 | {kind: "XV.ListAttr", attr: "totalHours", formatter: "formatHours", 58 | classes: "right"} 59 | ]} 60 | ]}, 61 | {kind: "XV.ListColumn", classes: "last", fit: true, components: [ 62 | {kind: "XV.ListAttr", attr: "toInvoice", formatter: "formatInvoice", 63 | classes: "italic"}, 64 | {kind: "XV.ListAttr", attr: "toVoucher", formatter: "formatVoucher"} 65 | ]} 66 | ]} 67 | ]} 68 | ], 69 | formatHours: XV.ProjectList.prototype.formatHours, 70 | formatInvoice: function (value, view, model) { 71 | var invoiced = model.get("invoiced"); 72 | if (!value && invoiced) { return "_invoiced".loc(); } 73 | view.addRemoveClass("placeholder", invoiced !== false); 74 | var scale = XT.locale.currencyScale; 75 | return invoiced === false ? Globalize.format(value, "c" + scale) : "_noInvoice".loc(); 76 | }, 77 | formatPosted: function (value) { 78 | return value ? "_posted".loc() : ""; 79 | }, 80 | formatStatus: function (value, view, model) { 81 | var status = model.get("worksheetStatus"); 82 | view.addRemoveClass("warn", status === XM.Worksheet.OPEN); 83 | view.addRemoveClass("emphasis", status === XM.Worksheet.APPROVED); 84 | return value; 85 | }, 86 | formatVoucher: function (value, view, model) { 87 | var vouchered = model.get("vouchered"); 88 | if (!value && vouchered) { return "_vouchered".loc(); } 89 | view.addRemoveClass("placeholder", !value); 90 | var scale = XT.locale.currencyScale; 91 | return value ? Globalize.format(value, "c" + scale) : "_noVoucher".loc(); 92 | } 93 | }); 94 | 95 | XV.registerModelList("XM.WorksheetListItem", "XV.WorksheetList"); 96 | }; 97 | 98 | }()); 99 | -------------------------------------------------------------------------------- /source/time_expense/client/widgets/project.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true, strict:false*/ 4 | /*global XT:true, XM:true, XV:true, enyo:true*/ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initProjectWidgets = function () { 9 | 10 | // .......................................................... 11 | // PROJECT 12 | // 13 | 14 | enyo.kind({ 15 | name: "XV.TaskWidget", 16 | published: { 17 | attr: null, 18 | value: null, 19 | disabled: false 20 | }, 21 | events: { 22 | "onValueChange": "" 23 | }, 24 | handlers: { 25 | "onValueChange": "controlValueChanged" 26 | }, 27 | components: [ 28 | {kind: "FittableRows", components: [ 29 | {kind: "XV.ProjectWidget", name: "project", label: "_project".loc(), 30 | collection: "XM.WorksheetProjectRelationCollection", 31 | style: "border-bottom-color: rgb(170, 170, 170); border-bottom-width: 1px; border-bottom-style: solid;", 32 | query: {parameters: [{attribute: "status", value: XM.Project.IN_PROCESS}]}}, 33 | {kind: "XV.PickerWidget", name: "tasks", label: "_task".loc(), 34 | nameAttribute: "formatNumber", orderBy: [{ attribute: "number" }]} 35 | ]} 36 | ], 37 | clear: function () { 38 | this.$.project.clear(); 39 | this.$.tasks.setValue(null); 40 | }, 41 | controlValueChanged: function (inSender, inEvent) { 42 | var project = this.$.project.getValue(), 43 | tasksPicker = this.$.tasks, 44 | tasks, 45 | task; 46 | if (inEvent.originator.name === "project") { 47 | if (project) { 48 | tasks = project.get("tasks"); 49 | tasksPicker._collection = tasks; 50 | tasksPicker.orderByChanged(); 51 | tasksPicker._collection.sort(); 52 | } else { 53 | tasksPicker._collection = []; 54 | } 55 | tasksPicker.buildList(); 56 | tasksPicker.clear({silent: true}); 57 | return true; 58 | } else if (inEvent.originator.name === "tasks") { 59 | tasks = project.get("tasks"); 60 | task = tasks.get(inEvent.value); 61 | inEvent = { originator: this, value: task.toJSON() }; 62 | this.doValueChange(inEvent); 63 | return true; 64 | } 65 | }, 66 | disabledChanged: function () { 67 | var disabled = this.getDisabled(); 68 | this.$.project.setDisabled(disabled); 69 | this.$.tasks.setDisabled(disabled); 70 | }, 71 | setValue: function (value, options) { 72 | options = options || {}; 73 | var oldValue = this.getValue(), 74 | inEvent; 75 | if (oldValue !== value) { 76 | this.value = value; 77 | this.valueChanged(value); 78 | inEvent = { value: value, originator: this }; 79 | if (!options.silent) { this.doValueChange(inEvent); } 80 | } 81 | }, 82 | valueChanged: function (value) { 83 | var that = this, 84 | project, 85 | number, 86 | tasksPicker = this.$.tasks, 87 | tasks, 88 | processTasks = function () { 89 | var options = {}; 90 | that.$.project.setValue(project, {silent: true}); 91 | options.success = function () { 92 | tasks = project.get("tasks"); 93 | tasksPicker._collection = tasks; 94 | tasksPicker.orderByChanged(); 95 | tasksPicker._collection.sort(); 96 | tasksPicker.buildList(); 97 | tasksPicker.clear({silent: true}); 98 | tasksPicker.setValue(value, {silent: true}); 99 | }; 100 | project.fetchRelated("tasks", options); 101 | }; 102 | if (value) { 103 | number = value.get("project"); 104 | if (number instanceof XM.Model) { number = number.id; } 105 | project = this.$.project.getValue(); 106 | if (!project || !project.id || project.id !== number) { 107 | project = XM.WorksheetProjectRelation.findOrCreate({number: number}); 108 | if (project.getStatus !== XM.Model.READY_CLEAN) { 109 | project.fetch({success: processTasks}); 110 | } else { 111 | processTasks(); 112 | } 113 | } else { 114 | processTasks(); 115 | } 116 | } else { 117 | this.$.project.clear(); 118 | this.$.tasks.clear(); 119 | } 120 | } 121 | }); 122 | }; 123 | 124 | }()); 125 | -------------------------------------------------------------------------------- /source/time_expense/database/orm/ext/project.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context": "time_expense", 4 | "nameSpace": "XM", 5 | "type": "Project", 6 | "table": "xt.teprjinfo", 7 | "isExtension": true, 8 | "comment": "Extended by Time Expense", 9 | "relations": [ 10 | { 11 | "column": "teprj_prj_id", 12 | "inverse": "id" 13 | } 14 | ], 15 | "properties": [ 16 | { 17 | "name": "customer", 18 | "toOne": { 19 | "isNested": true, 20 | "type": "CustomerRelation", 21 | "column": "teprj_cust_id" 22 | } 23 | }, 24 | { 25 | "name": "isSpecifiedRate", 26 | "attr": { 27 | "type": "Boolean", 28 | "column": "teprj_specified_rate" 29 | } 30 | }, 31 | { 32 | "name": "billingRate", 33 | "attr": { 34 | "type": "Number", 35 | "column": "teprj_rate" 36 | } 37 | }, 38 | { 39 | "name": "billingCurrency", 40 | "toOne": { 41 | "type": "Currency", 42 | "column": "teprj_curr_id" 43 | } 44 | } 45 | ], 46 | "isSystem": true 47 | }, 48 | { 49 | "context": "time_expense", 50 | "nameSpace": "XM", 51 | "type": "ProjectTask", 52 | "table": "xt.teprjtaskinfo", 53 | "isExtension": true, 54 | "comment": "Extended by Time Expense", 55 | "relations": [ 56 | { 57 | "column": "teprjtask_prjtask_id", 58 | "inverse": "id" 59 | } 60 | ], 61 | "properties": [ 62 | { 63 | "name": "item", 64 | "toOne": { 65 | "isNested": true, 66 | "type": "ItemRelation", 67 | "column": "teprjtask_item_id" 68 | } 69 | }, 70 | { 71 | "name": "customer", 72 | "toOne": { 73 | "isNested": true, 74 | "type": "CustomerRelation", 75 | "column": "teprjtask_cust_id" 76 | } 77 | }, 78 | { 79 | "name": "isSpecifiedRate", 80 | "attr": { 81 | "type": "Boolean", 82 | "column": "teprjtask_specified_rate" 83 | } 84 | }, 85 | { 86 | "name": "billingRate", 87 | "attr": { 88 | "type": "Number", 89 | "column": "teprjtask_rate" 90 | } 91 | }, 92 | { 93 | "name": "billingCurrency", 94 | "toOne": { 95 | "type": "Currency", 96 | "column": "teprjtask_curr_id" 97 | } 98 | } 99 | ], 100 | "isSystem": true 101 | }, 102 | { 103 | "context": "time_expense", 104 | "nameSpace": "XM", 105 | "type": "ProjectTaskRelation", 106 | "table": "xt.teprjtaskinfo", 107 | "isExtension": true, 108 | "comment": "Extended by Time Expense", 109 | "relations": [ 110 | { 111 | "column": "teprjtask_prjtask_id", 112 | "inverse": "id" 113 | } 114 | ], 115 | "properties": [ 116 | { 117 | "name": "item", 118 | "toOne": { 119 | "isNested": true, 120 | "type": "ItemRelation", 121 | "column": "teprjtask_item_id" 122 | } 123 | }, 124 | { 125 | "name": "customer", 126 | "toOne": { 127 | "isNested": true, 128 | "type": "CustomerRelation", 129 | "column": "teprjtask_cust_id" 130 | } 131 | } 132 | ], 133 | "isSystem": true 134 | }, 135 | { 136 | "context": "time_expense", 137 | "nameSpace": "XM", 138 | "type": "TaskRelation", 139 | "table": "xt.teprjtaskinfo", 140 | "isExtension": true, 141 | "comment": "Extended by Time Expense", 142 | "relations": [ 143 | { 144 | "column": "teprjtask_prjtask_id", 145 | "inverse": "id" 146 | } 147 | ], 148 | "properties": [ 149 | { 150 | "name": "item", 151 | "toOne": { 152 | "isNested": true, 153 | "type": "ItemRelation", 154 | "column": "teprjtask_item_id" 155 | } 156 | }, 157 | { 158 | "name": "customer", 159 | "toOne": { 160 | "isNested": true, 161 | "type": "CustomerRelation", 162 | "column": "teprjtask_cust_id" 163 | } 164 | }, 165 | { 166 | "name": "isSpecifiedRate", 167 | "attr": { 168 | "type": "Boolean", 169 | "column": "teprjtask_specified_rate" 170 | } 171 | }, 172 | { 173 | "name": "billingRate", 174 | "attr": { 175 | "type": "Number", 176 | "column": "teprjtask_rate" 177 | } 178 | }, 179 | { 180 | "name": "billingCurrency", 181 | "toOne": { 182 | "type": "Currency", 183 | "column": "teprjtask_curr_id" 184 | } 185 | } 186 | ], 187 | "isSystem": true 188 | } 189 | ] 190 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/invoicesheets.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.invoicesheets(integer[]) RETURNS integer AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | pHeadIDs ALIAS FOR $1; 6 | 7 | _invcnum text; 8 | _invcheadid integer; 9 | _invcitemid integer; 10 | _s record; 11 | _t record; 12 | _linenum integer; 13 | 14 | BEGIN 15 | -- Loop through time sheet items with matching criteria and make invoices 16 | FOR _s in SELECT DISTINCT 17 | teitem_cust_id, 18 | teitem_po, 19 | prj_id, 20 | teitem_curr_id 21 | FROM te.tehead 22 | JOIN te.teitem ON (teitem_tehead_id=tehead_id AND teitem_billable) 23 | JOIN prjtask ON (teitem_prjtask_id=prjtask_id) 24 | JOIN prj ON (prjtask_prj_id=prj_id) 25 | WHERE ((tehead_id IN (SELECT * FROM te.unnest(pHeadIDs) ) ) 26 | AND (teitem_billable) 27 | AND (teitem_invcitem_id IS NULL)) 28 | 29 | -- loop thru records and create invoices by customer, by PO for the provided headid 30 | LOOP 31 | --select nextval('invchead_invchead_id_seq') into _invcid; 32 | _invcnum := CAST(fetchInvcNumber() AS TEXT); 33 | _invcheadid := nextval('invchead_invchead_id_seq'); 34 | _linenum := 1; 35 | 36 | INSERT INTO invchead 37 | SELECT _invcheadid, cust_id, -1, '', current_date, false, false, _invcnum, 38 | current_date, current_date, _s.teitem_po, '', '', cust_name, COALESCE(addr_line1,''), 39 | COALESCE(addr_line2,''), COALESCE(addr_line3,''), COALESCE(addr_city,''), 40 | COALESCE(addr_state,''), COALESCE(addr_postalcode,''), cntct_phone, 41 | '', '', '', '', '', '', '', '', cust_salesrep_id, salesrep_commission, cust_terms_id, 42 | 0, 0, '', -1, 0, '', '', COALESCE(addr_country,''), '', _s.prj_id, 43 | _s.teitem_curr_id, current_date, false, null, null, null, null, null, cust_taxzone_id 44 | FROM custinfo 45 | JOIN salesrep ON (cust_salesrep_id=salesrep_id) 46 | LEFT OUTER JOIN cntct ON (cust_cntct_id=cntct_id) 47 | LEFT OUTER JOIN addr ON (cntct_addr_id=addr_id) 48 | WHERE (cust_id=_s.teitem_cust_id); 49 | 50 | -- loop thru all lines of the sheet 51 | FOR _t IN SELECT 52 | teitem_id, 53 | teitem_linenumber, 54 | tehead_warehous_id, 55 | teitem_type, 56 | tehead_emp_id, 57 | cust_taxzone_id, 58 | item_number, 59 | teitem_cust_id, 60 | teitem_po, 61 | teitem_item_id, 62 | teitem_qty, 63 | teitem_uom_id, 64 | teitem_rate, 65 | teitem_notes 66 | FROM te.teitem 67 | JOIN te.tehead ON (teitem_tehead_id = tehead_id) 68 | JOIN custinfo ON (cust_id = teitem_cust_id) 69 | JOIN item ON (item_id = teitem_item_id) 70 | JOIN prjtask ON (teitem_prjtask_id=prjtask_id) 71 | JOIN prj ON (prjtask_prj_id=prj_id) 72 | WHERE ((tehead_id IN (SELECT * FROM te.unnest(pHeadIDs) ) ) 73 | AND (teitem_billable) 74 | AND (teitem_invcitem_id IS NULL) 75 | AND (item_id = teitem_item_id) 76 | AND (teitem_cust_id = _s.teitem_cust_id) 77 | AND (teitem_po = _s.teitem_po) 78 | AND (prj_id = _s.prj_id) 79 | AND (teitem_curr_id = _s.teitem_curr_id)) 80 | ORDER BY teitem_linenumber 81 | LOOP 82 | _invcitemid := nextval('invcitem_invcitem_id_seq'); 83 | 84 | INSERT INTO invcitem 85 | SELECT 86 | _invcitemid, _invcheadid, _linenum, _t.teitem_item_id, 87 | _t.tehead_warehous_id, '', '', '', _t.teitem_qty, _t.teitem_qty, _t.teitem_rate, 88 | _t.teitem_rate, _t.teitem_notes, -1, getItemTaxType(item_id, _t.cust_taxzone_id), 89 | _t.teitem_uom_id, itemuomtouomratio(item_id, _t.teitem_uom_id, item_inv_uom_id), 90 | _t.teitem_uom_id, itemuomtouomratio(item_id, _t.teitem_uom_id, item_inv_uom_id), 91 | null 92 | FROM item 93 | WHERE (item_id=_t.teitem_item_id); 94 | 95 | _linenum := _linenum + 1; 96 | 97 | -- Update the time sheet item record 98 | UPDATE te.teitem SET teitem_invcitem_id = _invcitemid WHERE (teitem_id = _t.teitem_id); 99 | 100 | END LOOP; 101 | END LOOP; 102 | 103 | RETURN 1; 104 | END; 105 | $$ LANGUAGE 'plpgsql'; 106 | -------------------------------------------------------------------------------- /source/time_expense/client/models/project.js: -------------------------------------------------------------------------------- 1 | /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, 2 | newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true, 3 | white:true*/ 4 | /*global XT:true, XM:true*/ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.timeExpense.initProjectModels = function () { 10 | 11 | // .......................................................... 12 | // PROJECT 13 | // 14 | 15 | var _proto = XM.Project.prototype, 16 | _bindEvents = _proto.bindEvents, 17 | _statusDidChange = _proto.statusDidChange, 18 | _used = XM.Project.used, 19 | _specifiedSetReadOnly = function () { 20 | var spec = this.get("isSpecifiedRate"); 21 | this.setReadOnly("billingRate", !spec); 22 | this.setReadOnly("billingCurrency", !spec); 23 | }; 24 | 25 | XM.Project = XM.Project.extend({ 26 | 27 | bindEvents: function () { 28 | _bindEvents.apply(this, arguments); 29 | this.on("change:isSpecifiedRate", this.isSpecifiedRateDidChange); 30 | }, 31 | 32 | isSpecifiedRateDidChange: function () { 33 | var spec = this.get("isSpecifiedRate"); 34 | if (spec) { 35 | this.set("billingRate", 0); 36 | this.set("billingCurrency", XT.baseCurrency()); 37 | } else { 38 | this.set("billingRate", null); 39 | this.set("billingCurrency", null); 40 | } 41 | _specifiedSetReadOnly.apply(this); 42 | }, 43 | 44 | statusDidChange: function () { 45 | _statusDidChange.apply(this, arguments); 46 | var K = XM.Model, 47 | status = this.getStatus(); 48 | if (status === K.READY_NEW || status === K.READY_CLEAN) { 49 | _specifiedSetReadOnly.apply(this); 50 | } 51 | } 52 | 53 | }); 54 | 55 | /** 56 | Check to see if any worksheets use project since that isn't 57 | captured by usual algorithm. If not, run the normal check. 58 | */ 59 | XM.Project.used = function (id, options) { 60 | var that = this, 61 | dispOptions = { 62 | success: function (resp) { 63 | if (resp) { 64 | options.success(resp); 65 | } else { 66 | _used.call(that, id, options); 67 | } 68 | } 69 | }; 70 | XM.ModelMixin.dispatch("XM.Project", "worksheetUsed", 71 | [id], dispOptions); 72 | }; 73 | 74 | // .......................................................... 75 | // PROJECT TASK 76 | // 77 | 78 | // Unfortunately classes below can't share much with above because the private functions are different 79 | var _ptProto = XM.ProjectTask.prototype, 80 | _ptBindEvents = _ptProto.bindEvents, 81 | _ptStatusDidChange = _ptProto.statusDidChange; 82 | 83 | XM.ProjectTask = XM.ProjectTask.extend({ 84 | 85 | bindEvents: function () { 86 | _ptBindEvents.apply(this, arguments); 87 | this.on("change:isSpecifiedRate", this.isSpecifiedRateDidChange); 88 | }, 89 | 90 | formatNumber: function () { 91 | var number = this.get("number"), 92 | name = this.get("name"); 93 | return name ? number + " - " + name : number; 94 | }, 95 | 96 | isSpecifiedRateDidChange: function () { 97 | var spec = this.get("isSpecifiedRate"); 98 | if (spec) { 99 | this.set("billingRate", 0); 100 | this.set("billingCurrency", XT.baseCurrency()); 101 | } else { 102 | this.set("billingRate", null); 103 | this.set("billingCurrency", null); 104 | } 105 | _specifiedSetReadOnly.apply(this); 106 | }, 107 | 108 | statusDidChange: function () { 109 | _ptStatusDidChange.apply(this, arguments); 110 | var K = XM.Model, 111 | status = this.getStatus(); 112 | if (status === K.READY_NEW || status === K.READY_CLEAN) { 113 | _specifiedSetReadOnly.apply(this); 114 | } 115 | } 116 | 117 | }); 118 | 119 | XM.ProjectTaskRelation = XM.ProjectTaskRelation.extend({ 120 | 121 | formatNumber: function () { 122 | var number = this.get("number"), 123 | name = this.get("name"); 124 | return name ? number + " - " + name : number; 125 | } 126 | 127 | }); 128 | 129 | 130 | /** 131 | @class 132 | 133 | @extends XM.ProjectRelation 134 | */ 135 | XM.WorksheetProjectRelation = XM.ProjectRelation.extend({ 136 | /** @scope XM.WorksheetProjectRelation.prototype */ 137 | 138 | recordType: "XM.WorksheetProjectRelation", 139 | 140 | editableModel: "XM.Project" 141 | 142 | }); 143 | 144 | // .......................................................... 145 | // COLLECTIONS 146 | // 147 | 148 | /** 149 | @class 150 | 151 | @extends XM.Collection 152 | */ 153 | XM.WorksheetProjectRelationCollection = XM.Collection.extend({ 154 | /** @scope XM.WorksheetProjectRelationCollection.prototype */ 155 | 156 | model: XM.WorksheetProjectRelation 157 | 158 | }); 159 | 160 | }; 161 | 162 | }()); 163 | -------------------------------------------------------------------------------- /test/routes/routebi/olapdata.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true, 2 | regexp:true, undef:true, strict:true, trailing:true, white:true */ 3 | /*global X:true, XT:true, _:true, describe:true, it:true, before:true */ 4 | 5 | /* 6 | * olapdata tests the olapdata route running generic queries for the sum over all years checking 7 | * that some data is returned. It runs as a separate script (sudo npm run-script test-bi_open) 8 | * as the current directory must be set to node-datasource so it find config.js and the 9 | * the rest-keys folder. Also, the initial query can take a while build a cube in memory so the 10 | * timeout is set to 5000. The BI Server must be running. And the datasource must be running as 11 | * the BI server uses the REST API. 12 | * 13 | */ 14 | 15 | require("../../../../xtuple/node-datasource/xt"); 16 | var options = require("../../../../xtuple/node-datasource/lib/options"); 17 | 18 | // build X with the options needed by olapcatalog 19 | X.setup(options); 20 | X.options.datasource.debugging = true; 21 | 22 | var assert = require("chai").assert, 23 | olapRoute = require("../../../source/bi_open/node-datasource/routes/olapdata"), 24 | login = require("../../lib/login_data"); 25 | 26 | require("../../../source/bi_open/node-datasource/olapcatalog/olapcatalog"); 27 | require("../../../source/bi_open/node-datasource/olapcatalog/olapsource"); 28 | 29 | (function () { 30 | "use strict"; 31 | 32 | describe('The olapdata route', function () { 33 | 34 | it('should execute query to opportunities cube returning data', function (done) { 35 | // Mock the request object 36 | var req = { 37 | query: { 38 | mdx: "SELECT NON EMPTY {[Measures].[Amount, Opportunity Gross]} ON COLUMNS," + 39 | " NON EMPTY {Hierarchize({[Issue Date.Calendar].[All Years]})} ON ROWS" + 40 | " FROM [CROpportunity]" 41 | }, 42 | headers: {host: ""}, 43 | session: {passport: {user: {organization: login.data.org, username: login.data.username}}} 44 | }, 45 | // Mock the response object 46 | res = { 47 | responseText: {}, 48 | writeHead: function () {}, 49 | write: function (result) { 50 | this.responseText = JSON.parse(result); 51 | }, 52 | end: function () { 53 | assert.isNotNull(this.responseText.data[0]["[Measures].[Amount, Opportunity Gross]"]); 54 | done(); 55 | } 56 | }, 57 | addressTokens = []; 58 | addressTokens = login.data.webaddress.split("//"); 59 | req.headers.host = addressTokens[1] || "localhost:8842"; 60 | olapRoute.queryOlapCatalog(req, res); 61 | }); 62 | 63 | it('should execute query to opportunities forecast cube returning data', function (done) { 64 | // Mock the request object 65 | var req = { 66 | query: { 67 | mdx: "SELECT NON EMPTY {[Measures].[Amount, Opportunity Forecast]} ON COLUMNS," + 68 | " NON EMPTY {Hierarchize({[Fiscal Period.Fiscal Period CL].[All Years]})} ON ROWS" + 69 | " FROM [CROpportunityForecast]" 70 | }, 71 | headers: {host: ""}, 72 | session: {passport: {user: {organization: login.data.org, username: login.data.username}}} 73 | }, 74 | // Mock the response object 75 | res = { 76 | responseText: {}, 77 | writeHead: function () {}, 78 | write: function (result) { 79 | this.responseText = JSON.parse(result); 80 | }, 81 | end: function () { 82 | assert.isNotNull(this.responseText.data[0]["[Measures].[Amount, Opportunity Forecast]"]); 83 | done(); 84 | } 85 | }, 86 | addressTokens = []; 87 | addressTokens = login.data.webaddress.split("//"); 88 | req.headers.host = addressTokens[1] || "localhost:8842"; 89 | olapRoute.queryOlapCatalog(req, res); 90 | }); 91 | 92 | it('should execute query to quotes cube returning data', function (done) { 93 | // Mock the request object 94 | var req = { 95 | query: { 96 | mdx: "SELECT NON EMPTY {[Measures].[Amount, Quote Gross]} ON COLUMNS," + 97 | " NON EMPTY {Hierarchize({[Issue Date.Calendar].[All Years]})} ON ROWS" + 98 | " FROM [CRQuote]" 99 | }, 100 | headers: {host: ""}, 101 | session: {passport: {user: {organization: login.data.org, username: login.data.username}}} 102 | }, 103 | // Mock the response object 104 | res = { 105 | responseText: {}, 106 | writeHead: function () {}, 107 | write: function (result) { 108 | this.responseText = JSON.parse(result); 109 | }, 110 | end: function () { 111 | assert.isNotNull(this.responseText.data[0]["[Measures].[Amount, Quote Gross]"]); 112 | done(); 113 | } 114 | }, 115 | addressTokens = []; 116 | addressTokens = login.data.webaddress.split("//"); 117 | req.headers.host = addressTokens[1] || "localhost:8842"; 118 | olapRoute.queryOlapCatalog(req, res); 119 | }); 120 | 121 | }); 122 | }()); 123 | 124 | -------------------------------------------------------------------------------- /source/bi_open/client/widgets/bi_chart_measure.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, nv:true, d3:true, dimple:true console:true */ 5 | 6 | (function () { 7 | 8 | /** 9 | Implementation of BiChart with chart measure picker. Responsible for: 10 | - enyo components 11 | - picker management 12 | - filter management 13 | - requesting update of query templates based on pickers and filters 14 | - creating chart area 15 | */ 16 | enyo.kind( 17 | /** @lends XV.BiChartMeasure# */{ 18 | name: "XV.BiChartMeasure", 19 | kind: "XV.BiChart", 20 | published: { 21 | chartType: "barChart", 22 | chartTag: "svg", 23 | maxHeight: 0, 24 | maxWidth: 0, 25 | measures: [], 26 | // queryParms: 27 | measure: "", 28 | time: "", 29 | where: [], 30 | year: "current", 31 | month: "current", 32 | // May want to override these in the implementation 33 | parameterWidget: "XV.SalesChartParameters", 34 | initialChartTitle: "_chooseMeasure".loc() 35 | }, 36 | handlers: { 37 | onParameterChange: "parameterDidChange" 38 | }, 39 | chartControls: 40 | {kind: "enyo.FittableColumns", components: [ 41 | {content: "_measure".loc() + ": ", classes: "xv-picker-label"}, 42 | {kind: "onyx.PickerDecorator", onSelect: "measureSelected", 43 | components: [ 44 | {kind: "XV.PickerButton", content: "_chooseOne".loc()}, 45 | {name: "measurePicker", kind: "onyx.Picker"} 46 | ]} 47 | ]}, 48 | /** 49 | Populate the pickers and kickoff fetch of collections. 50 | */ 51 | create: function () { 52 | var that = this, 53 | model = this.getModel(); 54 | this.inherited(arguments); 55 | 56 | // Add controls to components. owner:this makes this the owner instead of 57 | // chartWrapper so the onSelect is handled by this. 58 | this.$.chartWrapper.createComponent({owner: this}, this.chartControls); 59 | 60 | // Populate the Measure picker from cubeMetaOverride or cubeMeta 61 | this.setMeasures(this.schema.getMeasures(this.getCube())); 62 | _.each(this.getMeasures(), function (item) { 63 | var pickItem = {name: item, content: ("_" + item).loc()}; 64 | that.$.measurePicker.createComponent(pickItem); 65 | }); 66 | 67 | // Set the measure and chart type from model 68 | if (model.get("measure")) { 69 | this.setMeasure(model.get("measure")); 70 | } 71 | this.setChartType(model.get("chartType") || "barChart"); 72 | 73 | // Set last filter uuid. This will drive fetchCollection if a filter is defined 74 | this.setLastFilter(); 75 | 76 | // If the measure is defined, fill in the queryTemplate 77 | // and ask the Collection to get data. 78 | if (this.getMeasure()) { 79 | this.updateQueries(); 80 | this.fetchCollection(); 81 | } 82 | }, 83 | /** 84 | Set chart component widths and heights using max sizes from dashboard - up to chart implementor. 85 | */ 86 | setComponentSizes: function (maxHeight, maxWidth) { 87 | var height = Number(maxHeight) - 20, 88 | width = Number(maxWidth) - 20; 89 | this.setMaxHeight(maxHeight); // for filterTapped to use later 90 | this.setMaxWidth(maxWidth); // for filterTapped to use later 91 | this.$.chartGroup.setStyle("width:" + width + "px;"); 92 | this.$.chartHeader.setStyle("width:" + width + "px;"); 93 | this.setStyle("width:" + width + "px;height:" + height + "px;"); // class selectable-chart 94 | this.$.chartWrapper.setStyle("width:" + width + "px;height:" + (height - 32) + "px;"); 95 | this.$.chartTitle.setStyle("width:" + width + "px;height:28px;"); 96 | this.$.chart.setStyle("width:" + width + "px;height:" + (height - 96) + "px;"); 97 | }, 98 | /** 99 | When the measure value changes, set the selected value 100 | in the picker widget, fetch the data and re-process the data. 101 | */ 102 | measureChanged: function () { 103 | var that = this, 104 | selected = _.find(this.$.measurePicker.controls, function (option) { 105 | return option.name === that.getMeasure(); 106 | }); 107 | this.$.measurePicker.setSelected(selected); 108 | this.updateQueries(); 109 | this.fetchCollection(); 110 | }, 111 | /** 112 | A new measure was selected in the picker. Set 113 | the published measure attribute. 114 | */ 115 | measureSelected: function (inSender, inEvent) { 116 | this.setMeasure(inEvent.originator.name); 117 | this.getModel().set("measure", inEvent.originator.name); 118 | this.save(this.getModel()); 119 | }, 120 | 121 | /* 122 | * Destroy and re-plot the chart area when the data changes. 123 | */ 124 | processedDataChanged: function () { 125 | this.createChartComponent(); 126 | this.plot(this.getChartType()); 127 | }, 128 | 129 | }); 130 | 131 | }()); 132 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/tables/teitem.sql: -------------------------------------------------------------------------------- 1 | -- table definition 2 | 3 | select xt.create_table('teitem', 'te'); 4 | 5 | -- remove old triggers if any 6 | drop trigger if exists teitemtrigger on te.teitem; 7 | drop trigger if exists teitem_did_change on te.teitem; 8 | 9 | select xt.add_column('teitem','teitem_id', 'serial', '', 'te'); 10 | select xt.add_column('teitem','teitem_tehead_id', 'integer', '', 'te'); 11 | select xt.add_column('teitem','teitem_linenumber', 'integer', 'not null', 'te'); 12 | select xt.add_column('teitem','teitem_type', 'character(1)', 'not null', 'te'); 13 | select xt.add_column('teitem','teitem_workdate', 'date', '', 'te'); 14 | select xt.add_column('teitem','teitem_cust_id', 'integer', '', 'te'); 15 | select xt.add_column('teitem','teitem_vend_id', 'integer', '', 'te'); 16 | select xt.add_column('teitem','teitem_po', 'text', '', 'te'); 17 | select xt.add_column('teitem','teitem_item_id', 'integer', 'not null', 'te'); 18 | select xt.add_column('teitem','teitem_qty', 'numeric', 'not null', 'te'); 19 | select xt.add_column('teitem','teitem_rate', 'numeric', 'not null', 'te'); 20 | select xt.add_column('teitem','teitem_total', 'numeric', 'not null', 'te'); 21 | select xt.add_column('teitem','teitem_prjtask_id', 'integer', 'not null', 'te'); 22 | select xt.add_column('teitem','teitem_lastupdated', 'timestamp without time zone', $$not null default ('now'::text)::timestamp(6) with time zone$$, 'te'); 23 | select xt.add_column('teitem','teitem_billable', 'boolean', '', 'te'); 24 | select xt.add_column('teitem','teitem_prepaid', 'boolean', '', 'te'); 25 | select xt.add_column('teitem','teitem_notes', 'text', '', 'te'); 26 | select xt.add_column('teitem','teitem_posted', 'boolean', 'default false', 'te'); 27 | select xt.add_column('teitem','teitem_curr_id', 'integer', 'not null default basecurrid()', 'te'); 28 | select xt.add_column('teitem','teitem_uom_id', 'integer', '', 'te'); 29 | select xt.add_column('teitem','teitem_invcitem_id', 'integer', '', 'te'); 30 | select xt.add_column('teitem','teitem_vodist_id', 'integer', '', 'te'); 31 | select xt.add_column('teitem','teitem_postedvalue', 'numeric', 'not null default 0', 'te'); 32 | select xt.add_column('teitem','teitem_empcost', 'numeric', '', 'te'); 33 | 34 | select xt.add_column('teitem','obj_uuid', 'uuid', 'default xt.uuid_generate_v4()', 'te'); 35 | select xt.add_inheritance('te.teitem', 'xt.obj'); 36 | select xt.add_constraint('teitem', 'teitem_obj_uuid','unique(obj_uuid)', 'te'); 37 | 38 | select xt.add_primary_key('teitem', 'teitem_id', 'te'); 39 | select xt.add_constraint('teitem', 'teitem_teitem_curr_id_fkey','foreign key (teitem_curr_id) references curr_symbol (curr_id) on delete set default', 'te'); 40 | select xt.add_constraint('teitem', 'teitem_teitem_invcitem_id_fkey','foreign key (teitem_invcitem_id) references invcitem (invcitem_id) on delete set null', 'te'); 41 | select xt.add_constraint('teitem', 'teitem_teitem_tehead_id_fkey','foreign key (teitem_tehead_id) references te.tehead (tehead_id)', 'te'); 42 | select xt.add_constraint('teitem', 'teitem_teitem_vodist_id_fkey','foreign key (teitem_vodist_id) references vodist (vodist_id) on delete set null', 'te'); 43 | 44 | -- Deal with problem where xtte package sets teitem_prjtask_id to wrong datatype to support correct fkey 45 | -- We've got to drop any views that depend on it first 46 | DO $$ 47 | var views, i, sql; 48 | var sql1 = "select data_type " + 49 | "from information_schema.columns " + 50 | "where table_name = 'teitem' " + 51 | "and table_schema = 'te' " + 52 | "and column_name = 'teitem_prjtask_id';"; 53 | var sql2 = "select distinct dependee_namespace.nspname || '.' || dependee.relname as viewname " + 54 | "from pg_depend " + 55 | "join pg_rewrite ON pg_depend.objid = pg_rewrite.oid " + 56 | "join pg_class as dependee ON pg_rewrite.ev_class = dependee.oid " + 57 | "join pg_class as dependent ON pg_depend.refobjid = dependent.oid " + 58 | "join pg_attribute ON pg_depend.refobjid = pg_attribute.attrelid " + 59 | " and pg_depend.refobjsubid = pg_attribute.attnum " + 60 | "join pg_namespace as dependent_namespace on dependent.relnamespace=dependent_namespace.oid " + 61 | "join pg_namespace as dependee_namespace on dependee.relnamespace=dependee_namespace.oid " + 62 | "where dependent_namespace.nspname = 'te' " + 63 | " and dependent.relname = 'teitem' " + 64 | " and pg_attribute.attnum > 0 " + 65 | " and pg_attribute.attname = 'teitem_prjtask_id';"; 66 | var sql3 = "drop view {viewname} cascade;"; 67 | var sql4 = "alter table te.teitem alter column teitem_prjtask_id set data type integer;"; 68 | 69 | // Find out if column is numeric 70 | if (plv8.execute(sql1)[0].data_type === 'numeric') { 71 | // Find and delete all dependent views 72 | views = plv8.execute(sql2); 73 | for (i = 0; i < views.length; i++) { 74 | sql = sql3.replace("{viewname}", views[i].viewname); 75 | plv8.execute(sql); 76 | } 77 | // Alter the type to integer 78 | plv8.execute(sql4); 79 | } 80 | $$ language plv8; 81 | select xt.add_constraint('teitem', 'teitem_teitem_prjtask_id_fkey','foreign key (teitem_prjtask_id) references prjtask (prjtask_id) ', 'te'); 82 | 83 | comment on table te.teitem is 'Time Expense Worksheet Item'; 84 | 85 | -- create triggers 86 | 87 | create trigger teitemtrigger after insert or update on te.teitem for each row execute procedure te.triggerteitem(); 88 | create trigger teitem_did_change after insert on te.teitem for each row execute procedure xt.teitem_did_change(); 89 | -------------------------------------------------------------------------------- /source/time_expense/database/source/te/functions/vouchersheet.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION te.vouchersheet(integer) RETURNS integer AS $$ 2 | -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 3 | -- See www.xtuple.com/CPAL for the full text of the software license. 4 | DECLARE 5 | pHeadID ALIAS FOR $1; 6 | _distamt NUMERIC; 7 | _glaccnt INTEGER; 8 | _notes TEXT; 9 | _s RECORD; 10 | _total NUMERIC := 0; 11 | _v RECORD; 12 | _vodistid INTEGER; 13 | _voheadid INTEGER; 14 | 15 | BEGIN 16 | FOR _v IN 17 | -- distinct filters duplicate rows returned because of the teitem join 18 | -- TODO: can we push the teitem down into the loop and avoid the distinct? 19 | SELECT DISTINCT 20 | tehead_id, tehead_number, tehead_weekending, tehead_notes, 21 | teitem_curr_id, 22 | emp_wage, emp_wage_period, 23 | vend_id, vend_taxzone_id, vend_terms_id, vend_number, vend_1099, 24 | COALESCE(teemp_contractor, false) AS isContractor 25 | FROM te.tehead 26 | JOIN te.teitem ON (teitem_tehead_id=tehead_id) 27 | JOIN emp ON (tehead_emp_id=emp_id) 28 | JOIN vendinfo ON (UPPER(emp_code)=UPPER(vend_number)) 29 | LEFT OUTER JOIN te.teemp ON (emp_id=teemp_emp_id) 30 | WHERE ((tehead_id = pHeadID) 31 | AND (teitem_prepaid = false) 32 | AND (teitem_vodist_id IS NULL) 33 | AND (teitem_type = 'E' OR (COALESCE(teemp_contractor,false) AND (teitem_empcost > 0 OR emp_wage > 0 )))) LOOP 34 | 35 | INSERT INTO vohead (vohead_id, vohead_number, vohead_vend_id, 36 | vohead_distdate, vohead_docdate, 37 | vohead_duedate, 38 | vohead_terms_id, vohead_taxzone_id, vohead_invcnumber, 39 | vohead_reference, vohead_amount, vohead_1099, 40 | vohead_curr_id, vohead_notes, vohead_posted, 41 | vohead_misc, vohead_pohead_id) 42 | VALUES (DEFAULT, fetchVoNumber(), _v.vend_id, 43 | _v.tehead_weekending, _v.tehead_weekending, 44 | determineDueDate(_v.vend_terms_id, _v.tehead_weekending), 45 | _v.vend_terms_id, _v.vend_taxzone_id, 'N/A', 46 | ('T&E Sheet ' || _v.tehead_number), 0, _v.vend_1099, 47 | _v.teitem_curr_id, _v.tehead_notes, false, 48 | true, -1) 49 | RETURNING vohead_id INTO _voheadid; 50 | 51 | FOR _s IN 52 | SELECT teitem_id, teitem_linenumber, teitem_workdate, teitem_type, 53 | item_number, teitem_item_id, teitem_qty, prjtask_prj_id, 54 | CASE 55 | WHEN teitem_empcost > 0 THEN teitem_empcost 56 | ELSE te.calcRate(_v.emp_wage, _v.emp_wage_period) 57 | END AS rate, 58 | teitem_total, teitem_type, teitem_notes, 59 | teexp_expcat_id, teexp_accnt_id 60 | FROM te.teitem 61 | JOIN te.teexp ON (teitem_item_id=teexp_id) 62 | JOIN item ON (teitem_item_id=item_id) 63 | JOIN prjtask ON (teitem_prjtask_id=prjtask_id) 64 | WHERE ((teitem_tehead_id = _v.tehead_id) 65 | AND (teitem_curr_id = _v.teitem_curr_id) 66 | AND (teitem_prepaid = false) 67 | AND (teitem_vodist_id IS NULL) 68 | AND (teitem_type = 'E' OR (_v.isContractor AND (teitem_empcost > 0 OR _v.emp_wage > 0 )))) 69 | 70 | 71 | -- Loop thru records and create vouchers by supplier for the provided headid 72 | LOOP 73 | -- insert vodist records here 74 | _vodistid = nextval('vodist_vodist_id_seq'); 75 | 76 | -- Map expense directly to account so we can get project account mapping if applicable 77 | IF (_s.teexp_accnt_id > 1) THEN 78 | _glaccnt := getPrjAccntId(_s.prjtask_prj_id, _s.teexp_accnt_id); 79 | ELSE 80 | SELECT getPrjAccntId(_s.prjtask_prj_id, expcat_exp_accnt_id) INTO _glaccnt 81 | FROM expcat 82 | WHERE (expcat_id=_s.teexp_expcat_id); 83 | END IF; 84 | 85 | IF (_s.teitem_type = 'T') THEN -- Time sheet record 86 | _notes := formatdate(_s.teitem_workdate) || E'\t' || _s.item_number || 87 | E'\t' || formatQty(_s.teitem_qty) || ' hours' || E'\t'; 88 | _distamt := _s.rate * _s.teitem_qty; 89 | ELSE -- Expense record 90 | _notes := formatdate(_s.teitem_workdate) || E'\t' || _s.item_number || 91 | E'\t' || _s.teitem_notes || E'\t'; 92 | _distamt := _s.teitem_total; 93 | END IF; 94 | 95 | INSERT INTO vodist (vodist_id, vodist_vohead_id, vodist_poitem_id, 96 | vodist_costelem_id, vodist_accnt_id, vodist_amount, 97 | vodist_expcat_id, vodist_notes) 98 | VALUES (_vodistid, _voheadid, -1, 99 | -1, _glaccnt, _distamt, 100 | -1, _notes); 101 | _total := _total + _distamt; 102 | 103 | -- Update the te.teitem record with the relationship 104 | UPDATE te.teitem SET teitem_vodist_id = _vodistid WHERE teitem_id = _s.teitem_id; 105 | END LOOP; 106 | 107 | UPDATE vohead SET vohead_amount = _total WHERE (vohead_id=_voheadid); 108 | _total := 0; 109 | 110 | END LOOP; 111 | 112 | RETURN 1; 113 | END; 114 | $$ LANGUAGE 'plpgsql' VOLATILE; 115 | -------------------------------------------------------------------------------- /source/time_expense/client/views/workspace.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, trailing:true, white:true*/ 3 | /*global XT:true, XM:true, XV:true, enyo:true*/ 4 | 5 | (function () { 6 | 7 | XT.extensions.timeExpense.initWorkspaces = function () { 8 | 9 | // .......................................................... 10 | // CUSTOMER 11 | // 12 | 13 | var customerExtensions = [ 14 | {kind: "onyx.GroupboxHeader", container: "settingsGroup", 15 | content: "_projectBilling".loc()}, 16 | {kind: "XV.ToggleButtonWidget", container: "settingsGroup", 17 | attr: "isSpecifiedRate"}, 18 | {kind: "XV.MoneyWidget", container: "settingsGroup", attr: 19 | {localValue: "billingRate", currency: "billingCurrency"}, 20 | label: "_rate".loc() } 21 | ]; 22 | 23 | XV.appendExtension("XV.CustomerWorkspace", customerExtensions); 24 | 25 | // .......................................................... 26 | // ITEM 27 | // 28 | 29 | var itemExtensions = [ 30 | {kind: "onyx.GroupboxHeader", container: "mainGroup", content: "_project".loc()}, 31 | {kind: "XV.ItemExpenseOptionsPicker", container: "mainGroup", label: "_expense".loc(), 32 | name: "itemExpenseOption", attr: "projectExpenseMethod"}, 33 | {kind: "XV.ExpenseCategoryPicker", container: "mainGroup", attr: "projectExpenseCategory", 34 | name: "expenseCategoryPicker", label: "_category".loc()}, 35 | {kind: "XV.LedgerAccountWidget", container: "mainGroup", attr: "projectExpenseLedgerAccount", 36 | name: "ledgerAccountWidget", label: "_account".loc(), 37 | query: {parameters: [{attribute: "accountType", operator: "ANY", 38 | value: [XM.LedgerAccount.ASSET, XM.LedgerAccount.LIABILITY, XM.LedgerAccount.EXPENSE]}]} 39 | } 40 | ]; 41 | 42 | XV.appendExtension("XV.ItemWorkspace", itemExtensions); 43 | 44 | // .......................................................... 45 | // PROJECT 46 | // 47 | 48 | var projectExtensions = [ 49 | {kind: "onyx.GroupboxHeader", container: "mainGroup", content: "_billing".loc()}, 50 | {kind: "XV.CustomerWidget", container: "mainGroup", attr: "customer"}, 51 | {kind: "XV.ToggleButtonWidget", container: "mainGroup", 52 | attr: "isSpecifiedRate"}, 53 | {kind: "XV.MoneyWidget", container: "mainGroup", attr: 54 | {localValue: "billingRate", currency: "billingCurrency"}, 55 | label: "_rate".loc() } 56 | ]; 57 | 58 | XV.appendExtension("XV.ProjectWorkspace", projectExtensions); 59 | 60 | // .......................................................... 61 | // PROJECT TASK 62 | // 63 | 64 | var taskExtensions = [ 65 | {kind: "onyx.GroupboxHeader", container: "mainGroup", content: "_billing".loc()}, 66 | {kind: "XV.ItemWidget", container: "mainGroup", attr: "item", 67 | query: {parameters: [ 68 | {attribute: "projectExpenseMethod", operator: "ANY", 69 | value: [XM.Item.EXPENSE_BY_CATEGORY, XM.Item.EXPENSE_BY_ACCOUNT] }, 70 | {attribute: "isActive", value: true} 71 | ]}}, 72 | {kind: "XV.CustomerWidget", container: "mainGroup", attr: "customer"}, 73 | {kind: "XV.ToggleButtonWidget", container: "mainGroup", 74 | attr: "isSpecifiedRate"}, 75 | {kind: "XV.MoneyWidget", container: "mainGroup", attr: 76 | {localValue: "billingRate", currency: "billingCurrency"}, 77 | label: "_rate".loc() } 78 | ]; 79 | 80 | XV.appendExtension("XV.ProjectTaskWorkspace", taskExtensions); 81 | 82 | // .......................................................... 83 | // EMPLOYEE 84 | // 85 | 86 | var employeeExtensions = [ 87 | {kind: "XV.CheckboxWidget", container: "detailGroup", 88 | attr: "isContractor"} 89 | ]; 90 | 91 | XV.appendExtension("XV.EmployeeWorkspace", employeeExtensions); 92 | 93 | // .......................................................... 94 | // WORKSHEET 95 | // 96 | 97 | enyo.kind({ 98 | name: "XV.WorksheetWorkspace", 99 | kind: "XV.Workspace", 100 | title: "_worksheet".loc(), 101 | model: "XM.Worksheet", 102 | components: [ 103 | {kind: "Panels", arrangerKind: "CarouselArranger", 104 | fit: true, components: [ 105 | {kind: "XV.Groupbox", name: "mainPanel", 106 | components: [ 107 | {kind: "onyx.GroupboxHeader", content: "_overview".loc()}, 108 | {kind: "XV.ScrollableGroupbox", name: "mainGroup", fit: true, 109 | classes: "in-panel", components: [ 110 | {kind: "XV.InputWidget", attr: "number"}, 111 | {kind: "XV.DateWidget", attr: "weekOf"}, 112 | {kind: "XV.EmployeeWidget", attr: "employee"}, 113 | {kind: "XV.UserAccountWidget", attr: "owner"}, 114 | {kind: "XV.SitePicker", attr: "site", fit: true}, 115 | {kind: "onyx.GroupboxHeader", content: "_notes".loc()}, 116 | {kind: "XV.TextArea", attr: "notes", fit: true}, 117 | {kind: "onyx.GroupboxHeader", content: "_total".loc()}, 118 | {kind: "XV.HoursWidget", attr: "totalHours", 119 | label: "_hours".loc()}, 120 | {kind: "XV.MoneyWidget", 121 | attr: {localValue: "totalExpenses", currency: "currency"}, 122 | label: "_expenses".loc() }, 123 | ]} 124 | ]}, 125 | {kind: "XV.WorksheetTimeBox", attr: "time"}, 126 | {kind: "XV.WorksheetExpenseBox", attr: "expenses"}, 127 | {kind: "XV.WorksheetDocumentsBox", attr: "documents"} 128 | ]} 129 | ] 130 | }); 131 | 132 | XV.registerModelWorkspace("XM.WorksheetListItem", "XV.WorksheetWorkspace"); 133 | }; 134 | 135 | }()); 136 | -------------------------------------------------------------------------------- /docs/TUTORIAL-FAQ.md: -------------------------------------------------------------------------------- 1 | ## xTuple Extension Tutorial FAQ 2 | 3 | ### How to fork and clone `xtuple-extensions` 4 | Log in to github.com (you will need a user account, which 5 | is free to set up), go to 6 | https://github.com/xtuple/xtuple-extensions, 7 | and click the "fork" button in the upper-right. 8 | 9 | Then, navigate on the command line of your development 10 | environment to the directory that contains the `xtuple` 11 | repository. Type 12 | 13 | ```bash 14 | git clone git@github.com:yourusername/xtuple-extensions.git 15 | ``` 16 | 17 | (with your own username, of course.) 18 | 19 | Congratulations! The `xtuple-extensions` directory is now 20 | created. It *is* important that this repository sits 21 | alongside the core `xtuple` repository. 22 | 23 | ### Why create a new schema? 24 | We do this to avoid namespace collisions, and generally to 25 | keep every extension code neatly cordoned off into its own 26 | area. It's easy enough to do and our `xm` (ORM) layer hides all 27 | of these details from the client. For the purposes of this 28 | tutorial all our tables will be in the `ic` schema, whereas 29 | the ORM-backed views will live in `xm`. 30 | 31 | ### Why not use native postgres functions to create tables? 32 | Our `xt.create_table` and `xt.add_column` functions ensure that 33 | you can run the same script over and over and not have to make 34 | separate install and update packages. The system will only 35 | add things that weren't there before. 36 | 37 | ### Where should I put ORM definitions? 38 | By convention, we put new orms in the `orm/models` directory, and 39 | extensions to existing orms in the `orm/ext` directory. Unlike with 40 | the sql scripts, you don't need to have a master file like the 41 | `manifest.js` that references them all. The core build tool will 42 | find all the files in these directories and load them in the 43 | appropriate order based on the dependency chain. 44 | 45 | ### What is wrapping the sample client code? 46 | At the beginning of all of our client-side files you'll see a jshint 47 | declaration, and the entirety of the code is wrapped in an anonymous 48 | function which is executed immediately. If possible, we also 49 | `"use strict"`. These are all good practices and you should 50 | follow them when writing in our style. Moreover, you'll also 51 | see that for the extension client-side files (except for `core.js` 52 | and the `package.js` files), the code is wrapped in another, named 53 | function that is not executed immediately. This allows us to load 54 | the code of the extension and actually execute the code later on 55 | in our setup process. 56 | 57 | For the sake of concision the code examples in this tutorial will 58 | ignore the jsdoc and anonymous function wrappers (your code will 59 | work without them), but you will see them in the actual 60 | implementation of this sample extension. 61 | 62 | You might find yourself copying and pasting the tops and bottoms 63 | of client-side files, so as to avoid writing the jshint, 64 | `"use strict"`, and wrapper functions. This is fine to do, 65 | but make sure that you rename the `XT.extensions` functions. 66 | There must only be one `XT.extensions.iceCream.initModels` 67 | [function](http://github.com/xtuple/xtuple-extensions/tree/master/sample/xtuple-ice-cream/client/models/ice_cream_flavor.js#L9), 68 | for example. 69 | 70 | ### What are the differences between the base model classes? 71 | We extend `XM.IceCreamFlavor` off of `XM.Document`, 72 | which is itself extended off of `XM.Model`, which itself extends 73 | Backbone-relational and Backbone models. `XM.Document` has some features 74 | on top of `XM.Model`, such as having a user-defined key. This is the 75 | most commonly extended base object in `backbone-x`. 76 | 77 | For the larger business objects, we'll typically define one or 78 | two lighterweight models (and ORMs) to be used in lists or as 79 | nested objects within other business objects. So, along with the 80 | "editable" model `XM.Quote`, we have `XM.QuoteListItem` and 81 | `XM.QuoteRelation`, which have fewer fields and are not editable. 82 | These lightweight models are extended from `XM.Info`. In the case 83 | of `IceCreamFlavor` our editable model is so light that we don't 84 | need to define any others. 85 | 86 | ### How do I update the strings file? 87 | 88 | The `/path/to/xtuple-extensions/source/xtuple-ice-cream/client/en/strings.js` file 89 | provides the English translations for all the visible text in your app. If you're 90 | diligent about always entering your visible text in the format "_myWord".loc(), 91 | then you'll notice the underscores onscreen, which will remind you to 92 | add the English translation into the `strings.js` file. 93 | 94 | By the end of your tutorial your `strings.js` file will look like 95 | [this](https://github.com/xtuple/xtuple-extensions/blob/master/sample/xtuple-ice-cream/client/en/strings.js). 96 | 97 | ### Why do we need a new table to extend `contact`? 98 | In a perfect world, we would just go into the `cntct` table and add a 99 | column. This is not an option. We're writing a humble extension here! 100 | We have no authority to make changes to core tables in the `public` schema. 101 | 102 | Adding a new table is next-easiest approach. The good news is that 103 | when we complete this plumbing in the database, the `Contact` business 104 | object will appear in the application as if this field were in it from 105 | the beginning. 106 | 107 | ### What is the `XM` collection cache? 108 | There are many collections of data that we want to be immediately 109 | accessible to the client, such as a list of the supported currencies, 110 | or incident priorities, or anything else that we'll want to use to fuel 111 | a `picker` dropdown. When we declare that we want a collection to 112 | be included in our cache, then it will be fetched when we start the app, 113 | and will be accessible as `XM.termsTypes`, for example. These caches 114 | are automatically kept up-to-date if you go to the setup area and change 115 | their models. 116 | 117 | -------------------------------------------------------------------------------- /docs/TUTORIAL4.md: -------------------------------------------------------------------------------- 1 | ## xTuple Extension Tutorial 2 | ### Part IV: Production deployment 3 | 4 | Writing a bit of code on your own laptop is one thing, but it won't do you much good if you can't deploy it into production. Unlike previous iterations of xTuple field dev, you can't feasibly use a tool like `pgadmin`, or even the classic xTuple `installer`, to get all of this code onto a production database. You might be tempted to `ssh` into the production server and recreate all of this work there, but that's not a good idea either, as doing so will make clean upgrades impossible. 5 | 6 | The appropriate way to deploy your code into production is to go by way of npm. The process is quite easy, actually, and has the added benefit of properly memorializing your source code for maximum reliability and reusability. 7 | 8 | ### A bit about npm 9 | 10 | You can think about npm as providing two services. It is a datacenter in California, with mirrors around the world, that hosts packages of code. It is also the software that provides nodejs-based project and dependency management. We rely on it heavily in every part of our app, and so using it for custom extension control was a natural fit. 11 | 12 | ### It's breathtakingly simple 13 | 14 | We at xTuple have already published `xtuple-ice-cream` to npm, so starting in xTuple version 4.6, all you have to do to install `xtuple-ice-cream` into your app is the following: 15 | 16 | - Boot up a new database without any of the work you've done so far, to mimic the production database 17 | - From the app home, click `Setup` -> `Configure` -> `Database` 18 | - You'll need the `Install Extensions` privilege, which you'll have automatically as an admin user in the `ADMIN` role 19 | - On the `Install Extension` panel, type `xtuple-ice-cream` and click the checkbox 20 | - When you get the success message, restart the browser and you'll see that the ice cream business objects are there 21 | - Best yet, when you upgrade your server, this extension will get reinstalled on the appropriate version automatically 22 | 23 | ### How do I publish my own npm package? 24 | 25 | Each npm package is defined by its `package.json` file. Look at code in the file `/path/to/xtuple-extensions/sample/xtuple-ice-cream/package.json`: 26 | ```js 27 | { 28 | "author": "xTuple ", 29 | "name": "xtuple-ice-cream", 30 | "description": "xTuple ice cream extension", 31 | "version": "0.1.3", 32 | "dependencies": { 33 | }, 34 | "peerDependencies": { 35 | "xtuple": "^4.7.0" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "http://github.com/xtuple/xtuple-extensions" 40 | }, 41 | "engines": { 42 | "node": "0.10.x" 43 | } 44 | } 45 | ``` 46 | 47 | Of course, you're not allowed to publish over our `xtuple-ice-cream` package, so if you copy this code into the file 48 | `/path/to/xtuple-extensions/source/xtuple-ice-cream/package.json` and try to publish, npm will not let you. Npm is a 49 | global registry, so if you want to practice publishing, choose another value for the `name` field in `package.json`. 50 | 51 | Publishing to npm is a process that's well-documented, and we do it the same way everyone else does. In short: 52 | 53 | ```bash 54 | cd /path/to/xtuple-extensions/source/xtuple-ice-cream 55 | npm publish ./ 56 | ``` 57 | 58 | At this point, npm will probably tell you to create an account and execute a few CLI commands, which you should do. 59 | Once you've published, your module will be immediately available on npmjs.org, and you'll immediately be able to 60 | install it from the database configuration screen. 61 | 62 | Note as well that the production use of npm for xTuple extension deployment also saves you from the requirement 63 | that all your work be done in your fork of the `xtuple-extensions` repository. All npm cares about is that the code 64 | is in a directory with a `package.json` file, so if you want to have a different Github (or not-Github) repository 65 | for each custom extension, you can do that too. See the [xtuple-morpheus](https://github.com/shackbarth/xtuple-morpheus) 66 | extension as an example of this kind of deployment strategy, and as an example for how to use the xTuple 67 | extension system to deploy third-party HTML and javascript libraries into the xTuple app. 68 | 69 | ### Version control 70 | 71 | Connecting the appropriate mainline version with the various versions of your extension as you update them 72 | is something that npm excels at, without the need for a human-readable dependency matrix to sweat over. This is 73 | done using `peerDependencies`. 74 | 75 | Let's say that some future version of the app (v4.30.0) forces or entices you to make a change to your extension. 76 | You'll need to 77 | 78 | - Write the new code 79 | - Bump the extension version appropriately in `package.json` (say, to `0.2.0`) 80 | - Change the `xtuple` `peerDependency` version from `"^4.7.0"` to `"^4.30.0"` 81 | - `npm publish` 82 | 83 | Now there will be two usable versions of your extension, and npm will prefer to install the latest version for 84 | which the `peerDependencies` match legally to the version of the main app. So if one user is on version `4.29.1` 85 | and installs your extension, npm will recognize your `0.1.2` as being off-limits, and will install `0.1.1`. 86 | But if at the same time another user is up to date at `4.30.0`, then npm will know to install your version 87 | `0.1.2`. 88 | 89 | It's furthermore possible to add *other* extensions into `peerDependencies`, with their own version 90 | requirements. However, so long as we keep our core extensions moving forward in lockstep with our core (a 91 | habit we don't intend to keep forever), this probably won't be necessary. 92 | 93 | ### Deploying private/proprietary custom extensions 94 | 95 | We're still working on that! We'll have a private solution, based around private github repos, available soon. 96 | 97 | ### Wrapping up 98 | 99 | That's it! Hopefully you have a sense of how to work within the xTuple Web/Mobile platform, and you're excited to start developing your own work. Drop us a line to let us know what you think, or if you have anything else you'd like to be better documented, at dev at xtuple dot com. 100 | -------------------------------------------------------------------------------- /source/time_expense/client/views/list_relations_editor_box.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XV:true, XM:true, _:true, Backbone:true, enyo:true, XT:true */ 5 | 6 | (function () { 7 | 8 | XT.extensions.timeExpense.initListRelationsEditorBox = function () { 9 | 10 | // .......................................................... 11 | // PROJECT TASK 12 | // 13 | 14 | var taskExtensions = [ 15 | {kind: "onyx.GroupboxHeader", container: "mainGroup", content: "_billing".loc()}, 16 | {kind: "XV.ItemWidget", container: "mainGroup", attr: "item", 17 | query: {parameters: [ 18 | {attribute: "projectExpenseMethod", operator: "ANY", 19 | value: [XM.Item.EXPENSE_BY_CATEGORY, XM.Item.EXPENSE_BY_ACCOUNT] }, 20 | {attribute: "isActive", value: true} 21 | ]}}, 22 | {kind: "XV.CustomerWidget", container: "mainGroup", attr: "customer"}, 23 | {kind: "XV.ToggleButtonWidget", container: "mainGroup", 24 | attr: "isSpecifiedRate"}, 25 | {kind: "XV.MoneyWidget", container: "mainGroup", attr: 26 | {localValue: "billingRate", currency: "billingCurrency"}, 27 | label: "_rate".loc() } 28 | ]; 29 | 30 | XV.appendExtension("XV.ProjectTaskEditor", taskExtensions); 31 | 32 | // .......................................................... 33 | // WORKSHEET 34 | // 35 | 36 | enyo.kind({ 37 | name: "XV.WorksheetTimeEditor", 38 | kind: "XV.RelationsEditor", 39 | components: [ 40 | {kind: "XV.ScrollableGroupbox", name: "mainGroup", fit: true, 41 | classes: "in-panel", components: [ 42 | {kind: "XV.InputWidget", attr: "lineNumber"}, 43 | {kind: "XV.DateWidget", attr: "workDate"}, 44 | {kind: "XV.HoursWidget", attr: "hours"}, 45 | {kind: "onyx.GroupboxHeader", content: "_detail".loc()}, 46 | {kind: "XV.TaskWidget", attr: "task"}, 47 | {kind: "XV.ItemWidget", attr: "item", 48 | query: {parameters: [ 49 | {attribute: "projectExpenseMethod", operator: "ANY", 50 | value: [XM.Item.EXPENSE_BY_CATEGORY, XM.Item.EXPENSE_BY_ACCOUNT] }, 51 | {attribute: "isActive", value: true} 52 | ]}}, 53 | {kind: "onyx.GroupboxHeader", content: "_billing".loc()}, 54 | {kind: "XV.CheckboxWidget", attr: "billable"}, 55 | {kind: "XV.CustomerWidget", attr: "customer"}, 56 | {kind: "XV.InputWidget", attr: "purchaseOrderNumber", 57 | label: "_custPo".loc()}, 58 | {kind: "XV.MoneyWidget", 59 | attr: {localValue: "billingRate", currency: "billingCurrency"}, 60 | label: "_rate".loc() }, 61 | {kind: "XV.MoneyWidget", 62 | attr: {localValue: "billingTotal", currency: "billingCurrency"}, 63 | label: "_total".loc(), currencyDisabled: true }, 64 | {kind: "onyx.GroupboxHeader", content: "_cost".loc(), name: "costHeader"}, 65 | {kind: "XV.MoneyWidget", 66 | attr: {localValue: "hourlyRate", currency: "hourlyCurrency"}, 67 | label: "_hourly".loc(), currencyDisabled: true }, 68 | {kind: "XV.MoneyWidget", 69 | attr: {localValue: "hourlyTotal", currency: "hourlyCurrency"}, 70 | label: "_total".loc(), currencyDisabled: true }, 71 | {kind: "onyx.GroupboxHeader", content: "_notes".loc()}, 72 | {kind: "XV.TextArea", attr: "notes", fit: true} 73 | ]} 74 | ], 75 | create: function () { 76 | this.inherited(arguments); 77 | // Don't show cost header if user doesn't have cost permissions 78 | this.$.costHeader.setShowing(XT.session.privileges.attributes.MaintainEmpCostAll); 79 | } 80 | }); 81 | 82 | enyo.kind({ 83 | name: "XV.WorksheetTimeBox", 84 | kind: "XV.ListRelationsEditorBox", 85 | title: "_time".loc(), 86 | editor: "XV.WorksheetTimeEditor", 87 | parentKey: "worksheet", 88 | listRelations: "XV.WorksheetTimeListRelations", 89 | /** 90 | Copies current task into next entry if applicable. 91 | */ 92 | newItem: function () { 93 | var widget = this.$.editor.$.taskWidget, 94 | task = widget.getValue(); 95 | this.inherited(arguments); 96 | if (task) { widget.setValue(task); } 97 | } 98 | }); 99 | 100 | enyo.kind({ 101 | name: "XV.WorksheetExpenseEditor", 102 | kind: "XV.RelationsEditor", 103 | components: [ 104 | {kind: "XV.ScrollableGroupbox", name: "mainGroup", fit: true, 105 | classes: "in-panel", components: [ 106 | {kind: "XV.InputWidget", attr: "lineNumber"}, 107 | {kind: "XV.DateWidget", attr: "workDate"}, 108 | {kind: "XV.QuantityWidget", attr: "quantity"}, 109 | {kind: "XV.MoneyWidget", 110 | attr: {localValue: "unitCost", currency: "billingCurrency"}, 111 | label: "_unitCost".loc() }, 112 | {kind: "XV.MoneyWidget", 113 | attr: {localValue: "billingTotal", currency: "billingCurrency"}, 114 | label: "_total".loc(), currencyDisabled: true }, 115 | {kind: "XV.CheckboxWidget", attr: "prepaid"}, 116 | {kind: "onyx.GroupboxHeader", content: "_detail".loc()}, 117 | {kind: "XV.TaskWidget", attr: "task"}, 118 | {kind: "XV.ItemWidget", attr: "item", 119 | query: {parameters: [ 120 | {attribute: "projectExpenseMethod", operator: "ANY", 121 | value: [XM.Item.EXPENSE_BY_CATEGORY, XM.Item.EXPENSE_BY_ACCOUNT] }, 122 | {attribute: "isActive", value: true} 123 | ]}}, 124 | {kind: "onyx.GroupboxHeader", content: "_billing".loc()}, 125 | {kind: "XV.CheckboxWidget", attr: "billable"}, 126 | {kind: "XV.CustomerWidget", attr: "customer"}, 127 | {kind: "XV.InputWidget", attr: "purchaseOrderNumber", 128 | label: "_custPo".loc()}, 129 | {kind: "onyx.GroupboxHeader", content: "_notes".loc()}, 130 | {kind: "XV.TextArea", attr: "notes", fit: true} 131 | ]} 132 | ] 133 | }); 134 | 135 | enyo.kind({ 136 | name: "XV.WorksheetExpenseBox", 137 | kind: "XV.ListRelationsEditorBox", 138 | title: "_expenses".loc(), 139 | editor: "XV.WorksheetExpenseEditor", 140 | parentKey: "worksheet", 141 | listRelations: "XV.WorksheetExpenseListRelations", 142 | /** 143 | Copies current task into next entry if applicable. 144 | */ 145 | newItem: function () { 146 | var widget = this.$.editor.$.taskWidget, 147 | task = widget.getValue(); 148 | this.inherited(arguments); 149 | if (task) { widget.setValue(task); } 150 | } 151 | }); 152 | 153 | }; 154 | 155 | }()); 156 | -------------------------------------------------------------------------------- /source/bi_open/client/core.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true, 2 | latedef:true, newcap:true, noarg:true, regexp:true, undef:true, 3 | trailing:true, white:true*/ 4 | /*global XT:true, XV:true, XM:true, enyo:true, _:true, console:true */ 5 | 6 | (function () { 7 | "use strict"; 8 | 9 | XT.extensions.bi_open = {}; 10 | 11 | _.extend(XT, { 12 | /* 13 | * All BI extensions add their collection of charts to this list. So all charts are 14 | * available to all dashboards. 15 | */ 16 | chartActions: [], 17 | /* 18 | * MDX Query Class 19 | * All BI extensions add their collection of maps to this list. So all maps are 20 | * available to all mapboards. 21 | */ 22 | mapActions: [], 23 | /* 24 | * MDX Query Class 25 | */ 26 | mdxQuery: function () { 27 | }, 28 | /* 29 | * Time Series Query 30 | */ 31 | mdxQueryTimeSeries: function () { 32 | }, 33 | /* 34 | * Top List Query 35 | */ 36 | mdxQueryTopList: function () { 37 | }, 38 | /* 39 | * Sum Periods Query 40 | */ 41 | mdxQuerySumPeriods: function () { 42 | }, 43 | /* 44 | * Map Periods Query 45 | */ 46 | mdxQueryMapPeriods: function () { 47 | } 48 | 49 | }); 50 | 51 | XT.mdxQuery.prototype = Object.create({ 52 | /* 53 | * Generate MDX query string based on queryTemplate. members are optional. 54 | * rows, columns, cube and where filters are required. Additional filters can 55 | * be added using the filters argument. 56 | */ 57 | jsonToMDX: function (filters) { 58 | var that = this, 59 | query = "", 60 | comma = "", 61 | filterSet = filters ? filters : []; 62 | 63 | // WITH MEMBERS clause 64 | filterSet = this.where ? filters.concat(this.where) : filterSet; 65 | _.each(this.members, function (member, index) { 66 | query = index === 0 ? "WITH " : query; 67 | query += " MEMBER " + member.name + " AS " + member.value; 68 | }); 69 | 70 | // SELECT clause 71 | query += " SELECT NON EMPTY {"; 72 | _.each(this.columns, function (column, index) { 73 | comma = index > 0 ? ", " : ""; 74 | query += comma + column; 75 | }); 76 | query += "} ON COLUMNS, NON EMPTY {"; 77 | _.each(this.rows, function (row, index) { 78 | comma = index > 0 ? ", " : ""; 79 | query += comma + row; 80 | }); 81 | query += "} ON ROWS"; 82 | 83 | // FROM clause 84 | query += " FROM " + this.cube; 85 | 86 | // WHERE clause 87 | _.each(filterSet, function (filter, index) { 88 | query = index === 0 ? query + " WHERE (" : query; 89 | comma = index > 0 ? ", " : ""; 90 | query += comma + filter.dimension + ".[" + filter.value + "]"; 91 | }); 92 | if (query.indexOf(" WHERE (") !== -1) { 93 | query += ")"; 94 | } 95 | 96 | return query; 97 | } 98 | }); 99 | 100 | XT.mdxQueryTimeSeries.prototype = _.extend(Object.create(XT.mdxQuery.prototype), { 101 | members: [ 102 | {name: "[Measures].[KPI]", 103 | value: "IIf(IsEmpty([Measures].[$measure]), 0.000, [Measures].[$measure])" 104 | }, 105 | {name: "Measures.[prevKPI]", 106 | value: "([Measures].[$measure] , ParallelPeriod([Issue Date.Calendar Months].[$year]))" 107 | }, 108 | {name: "[Measures].[prevYearKPI]", 109 | value: "iif (Measures.[prevKPI] = 0 or Measures.[prevKPI] = NULL or IsEmpty(Measures.[prevKPI]), 0.000, Measures.[prevKPI])" 110 | }, 111 | ], 112 | columns: [ 113 | "[Measures].[KPI]", 114 | "[Measures].[prevYearKPI]" 115 | ], 116 | rows: [ 117 | "LastPeriods(12, [Issue Date.Calendar Months].[$year].[$month])" 118 | ], 119 | cube: "", 120 | where: [] 121 | }); 122 | /* 123 | * Top list query. 124 | * 125 | * Note we are given a dimension's level, but we want the children, so we 126 | * go back up to the hierarchy and then get the children. 127 | * 128 | * Todo: Mondrian has trouble ordering with count measures, like "Days, Start to Actual". 129 | * So we sort in processData as the list is small. Try this out in later releases of Mondrian: 130 | * "ORDER({filter(TopCount($dimensionHier.Hierarchy.Children, 50, [Measures].[THESUM]),[Measures].[THESUM]>0) }, [Measures].[THESUM], DESC) 131 | */ 132 | XT.mdxQueryTopList.prototype = _.extend(Object.create(XT.mdxQuery.prototype), { 133 | members: [ 134 | {name: "[Measures].[NAME]", 135 | value: '$dimensionHier.CurrentMember.Properties("$dimensionNameProp")' 136 | }, 137 | {name: "[Measures].[THESUM]", 138 | value: "SUM({LASTPERIODS(12, [$dimensionTime].[$year].[$month])}, [Measures].[$measure])" 139 | }, 140 | ], 141 | columns: [ 142 | "[Measures].[THESUM]", 143 | "[Measures].[NAME]" 144 | ], 145 | rows: [ 146 | "{filter(TopCount($dimensionHier.Hierarchy.Children, 50, [Measures].[THESUM]),[Measures].[THESUM]>0)}" 147 | ], 148 | cube: "", 149 | where: [] 150 | }); 151 | 152 | XT.mdxQuerySumPeriods.prototype = _.extend(Object.create(XT.mdxQuery.prototype), { 153 | members: [ 154 | {name: "[Measures].[THESUM]", 155 | value: "SUM({LASTPERIODS(12, [Issue Date.Calendar].[$year].[$month])}, [Measures].[$measure])" 156 | }, 157 | ], 158 | columns: [ 159 | "[Measures].[THESUM]", 160 | ], 161 | rows: [ 162 | "Hierarchize({[Opportunity].[All Opportunities]})" 163 | ], 164 | cube: "", 165 | where: [] 166 | }); 167 | /* 168 | * Map Query. 169 | * 170 | * Note that cross joins of large dimensions like dimensionGeo are performance problems. Check that all 171 | * cross join options in mondrian.properties are set. Make sure heap space is sufficient in start_bi.sh 172 | * A cross joins of members performs well, but a cross join of children does not (mondrian bug?). 173 | * "CrossJoin($dimensionHier.Children, $dimensionGeo.Members)" - bad 174 | * "CrossJoin($dimensionHier.Members, $dimensionGeo.Members)" - good 175 | * All queries should be written as using members. 176 | */ 177 | XT.mdxQueryMapPeriods.prototype = _.extend(Object.create(XT.mdxQuery.prototype), { 178 | members: [ 179 | {name: "[Measures].[TheSum]", 180 | value: 'SUM({LASTPERIODS(12, [Issue Date.Calendar].[$year].[$month])}, [Measures].[Amount, Order Gross])' 181 | }, 182 | {name: "[Measures].[Longitude]", 183 | value: 'iif ([Measures].[TheSum] is empty, null, $dimensionGeo.CurrentMember.Properties("Longitude"))' 184 | }, 185 | {name: "[Measures].[Latitude]", 186 | value: 'iif ([Measures].[TheSum] is empty, null, $dimensionGeo.CurrentMember.Properties("Latitude"))' 187 | }, 188 | ], 189 | columns: [ 190 | "[Measures].[Latitude]", "[Measures].[Longitude]", "[Measures].[TheSum]", 191 | ], 192 | rows: [ 193 | "CrossJoin($dimensionHier.Members, $dimensionGeo.Members)" 194 | ], 195 | cube: "", 196 | where: [] 197 | }); 198 | 199 | }()); 200 | --------------------------------------------------------------------------------