From 3533376e837f6e175a8285f5999594c017a056f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cornelius=20Wei=C3=9F?= Date: Thu, 17 Oct 2024 09:28:59 +0200 Subject: [PATCH] feature(Sales): dispatch documents and evaluate document transport config --- tine20/Filemanager/js/QuickLookMediaPanel.js | 2 +- tine20/Sales/Frontend/Http.php | 15 +- tine20/Sales/Frontend/Json.php | 1 + tine20/Sales/Model/Document/Abstract.php | 13 +- .../Sales/Model/Document/AttachedDocument.php | 3 + .../Sales/Model/Document/DispatchHistory.php | 27 ++ tine20/Sales/css/Sales.css | 8 + tine20/Sales/js/Document/AbstractAction.js | 55 +++++ .../Sales/js/Document/BookDocumentAction.js | 96 ++++++++ .../Sales/js/Document/CreateFollowUpAction.js | 2 +- .../js/Document/CreatePaperSlipAction.js | 105 +++++--- .../js/Document/DispatchDocumentAction.js | 233 ++++++++++++++++++ tine20/Sales/js/Document/TracAction.js | 2 +- tine20/Sales/js/EDocument/QuickLookPanel.js | 28 +++ .../Sales/js/Model/Document/AbstractMixin.js | 6 + tine20/Sales/js/Sales.js | 3 + .../Sales/views/EDocumentViewError.html.twig | 23 ++ tine20/Tinebase/Twig.php | 3 + tine20/Tinebase/js/widgets/ActionUpdater.js | 2 +- tine20/composer.json | 1 + tine20/composer.lock | 187 +++++++++++++- tine20/images/icon-set | 2 +- 22 files changed, 768 insertions(+), 49 deletions(-) create mode 100644 tine20/Sales/js/Document/AbstractAction.js create mode 100644 tine20/Sales/js/Document/BookDocumentAction.js create mode 100644 tine20/Sales/js/Document/DispatchDocumentAction.js create mode 100644 tine20/Sales/js/EDocument/QuickLookPanel.js create mode 100644 tine20/Sales/views/EDocumentViewError.html.twig diff --git a/tine20/Filemanager/js/QuickLookMediaPanel.js b/tine20/Filemanager/js/QuickLookMediaPanel.js index b96b6a47268..2037ea8bde6 100644 --- a/tine20/Filemanager/js/QuickLookMediaPanel.js +++ b/tine20/Filemanager/js/QuickLookMediaPanel.js @@ -6,7 +6,7 @@ * @copyright Copyright (c) 2022 Metaways Infosystems GmbH (http://www.metaways.de) */ -MediaPanel = Ext.extend(Ext.Panel, { +const MediaPanel = Ext.extend(Ext.Panel, { border: false, initComponent: function() { diff --git a/tine20/Sales/Frontend/Http.php b/tine20/Sales/Frontend/Http.php index c9af5769488..1d1d908a932 100644 --- a/tine20/Sales/Frontend/Http.php +++ b/tine20/Sales/Frontend/Http.php @@ -26,9 +26,18 @@ class Sales_Frontend_Http extends Tinebase_Frontend_Http_Abstract public function getXRechnungView(string $fileNodeId): void { - echo (new Sales_EDocument_Service_View())->getXRechnungView( - Filemanager_Controller_Node::getInstance()->get($fileNodeId) - ); + try { + echo (new Sales_EDocument_Service_View())->getXRechnungView( + Filemanager_Controller_Node::getInstance()->get($fileNodeId) + ); + } catch (Exception $e) { + $twig = new Tinebase_Twig(Tinebase_Core::getLocale(), Tinebase_Translation::getTranslation('Sales')); + + $template = $twig->load('Sales/views/EDocumentViewError.html.twig'); + + $renderContext = []; + echo $template->render($renderContext); + } } /** diff --git a/tine20/Sales/Frontend/Json.php b/tine20/Sales/Frontend/Json.php index 6de63872c67..abcc564b0b3 100644 --- a/tine20/Sales/Frontend/Json.php +++ b/tine20/Sales/Frontend/Json.php @@ -69,6 +69,7 @@ class Sales_Frontend_Json extends Tinebase_Frontend_Json_Abstract Sales_Model_EDocument_EAS::MODEL_NAME_PART, Sales_Model_Einvoice_XRechnung::MODEL_NAME_PART, Sales_Model_Document_AttachedDocument::MODEL_NAME_PART, + Sales_Model_Document_DispatchHistory::MODEL_NAME_PART, // 'OrderConfirmation', // 'PurchaseInvoice', // 'Offer', diff --git a/tine20/Sales/Model/Document/Abstract.php b/tine20/Sales/Model/Document/Abstract.php index a1a0bbc3fc5..73f62886359 100644 --- a/tine20/Sales/Model/Document/Abstract.php +++ b/tine20/Sales/Model/Document/Abstract.php @@ -145,7 +145,11 @@ abstract class Sales_Model_Document_Abstract extends Tinebase_Record_NewAbstract 'cpintern_id' => [], ], ], - self::FLD_DEBITOR_ID => [], + self::FLD_DEBITOR_ID => [ + Tinebase_Record_Expander::EXPANDER_PROPERTIES => [ + Sales_Model_Debitor::FLD_EAS_ID => [], + ], + ], self::FLD_RECIPIENT_ID => [ Tinebase_Record_Expander::EXPANDER_PROPERTIES => [ Sales_Model_Address::FLD_DEBITOR_ID => [], @@ -156,7 +160,12 @@ abstract class Sales_Model_Document_Abstract extends Tinebase_Record_NewAbstract Sales_Model_DocumentPosition_Abstract::FLD_PRECURSOR_POSITION => [], ], ], - self::FLD_ATTACHED_DOCUMENTS => [], + self::FLD_ATTACHED_DOCUMENTS => [ + Tinebase_Record_Expander::EXPANDER_PROPERTIES => [ + Sales_Model_Document_AttachedDocument::FLD_DISPATCH_HISTORY => [], + ], + ], + self::FLD_CONTACT_ID => [], ] ], diff --git a/tine20/Sales/Model/Document/AttachedDocument.php b/tine20/Sales/Model/Document/AttachedDocument.php index 069f77ad207..32b83c0ad24 100644 --- a/tine20/Sales/Model/Document/AttachedDocument.php +++ b/tine20/Sales/Model/Document/AttachedDocument.php @@ -47,6 +47,8 @@ class Sales_Model_Document_AttachedDocument extends Tinebase_Record_NewAbstract self::APP_NAME => Sales_Config::APP_NAME, self::MODEL_NAME => self::MODEL_NAME_PART, + self::EXPOSE_JSON_API => true, + self::TABLE => [ self::NAME => self::TABLE_NAME, self::INDEXES => [ @@ -123,6 +125,7 @@ class Sales_Model_Document_AttachedDocument extends Tinebase_Record_NewAbstract self::FLD_DISPATCH_HISTORY => [ self::TYPE => self::TYPE_RECORDS, self::CONFIG => [ + self::DEPENDENT_RECORDS => true, self::APP_NAME => Sales_Config::APP_NAME, self::MODEL_NAME => Sales_Model_Document_DispatchHistory::MODEL_NAME_PART, self::REF_ID_FIELD => Sales_Model_Document_DispatchHistory::FLD_ATTACHED_DOCUMENT_ID, diff --git a/tine20/Sales/Model/Document/DispatchHistory.php b/tine20/Sales/Model/Document/DispatchHistory.php index a202a5d4a70..6f02b20f87f 100644 --- a/tine20/Sales/Model/Document/DispatchHistory.php +++ b/tine20/Sales/Model/Document/DispatchHistory.php @@ -22,6 +22,9 @@ class Sales_Model_Document_DispatchHistory extends Tinebase_Record_NewAbstract public const FLD_ATTACHED_DOCUMENT_ID = 'attached_document_id'; + public const FLD_DISPATCH_DATE = 'dispatch_date'; + public const FLD_DISPATCH_TRANSPORT = 'dispatch_transport'; + public const FLD_DISPATCH_REPORT = 'dispatch_report'; /** * Holds the model configuration (must be assigned in the concrete class) @@ -32,6 +35,7 @@ class Sales_Model_Document_DispatchHistory extends Tinebase_Record_NewAbstract self::VERSION => 1, self::MODLOG_ACTIVE => true, self::IS_DEPENDENT => true, + self::HAS_ATTACHMENTS => true, self::APP_NAME => Sales_Config::APP_NAME, self::MODEL_NAME => self::MODEL_NAME_PART, @@ -46,6 +50,29 @@ class Sales_Model_Document_DispatchHistory extends Tinebase_Record_NewAbstract ], self::FIELDS => [ + self::FLD_DISPATCH_DATE => [ + self::LABEL => 'Dispatch Date', //_('Dispatch Date') + self::TYPE => self::TYPE_DATE, + self::NULLABLE => false, + self::UI_CONFIG => [ + 'format' => ['medium'], + ], + ], + self::FLD_DISPATCH_TRANSPORT => [ + self::TYPE => self::TYPE_KEY_FIELD, + self::LABEL => 'Dispatch Transport Method', // _('Dispatch Transport Method') + self::NAME => Sales_Config::EDOCUMENT_TRANSPORT, + self::NULLABLE => false, + self::CONFIG => [ + self::APP_NAME => Sales_Config::APP_NAME, + ], + ], + self::FLD_DISPATCH_REPORT => [ + self::LABEL => 'Dispatch Report', //_('Dispatch Report') + self::TYPE => self::TYPE_TEXT, + self::NULLABLE => true, + self::QUERY_FILTER => true, + ], self::FLD_ATTACHED_DOCUMENT_ID => [ self::TYPE => self::TYPE_RECORD, self::NULLABLE => true, diff --git a/tine20/Sales/css/Sales.css b/tine20/Sales/css/Sales.css index 45a354f3bca..bbb9dc0f17c 100644 --- a/tine20/Sales/css/Sales.css +++ b/tine20/Sales/css/Sales.css @@ -44,6 +44,14 @@ background-image:url(../../images/icon-set/icon_invoice_cancle.svg) !important; } +.action_dispatch_document { + background-image:url(../../images/icon-set/icon_email_out.svg) !important; +} + +.action_book_document { + background-image:url(../../images/icon-set/icon_booked.svg) !important; +} + .action_rebill { background-image:url(../../images/icon-set/icon_return.svg) !important; } diff --git a/tine20/Sales/js/Document/AbstractAction.js b/tine20/Sales/js/Document/AbstractAction.js new file mode 100644 index 00000000000..88010c941f5 --- /dev/null +++ b/tine20/Sales/js/Document/AbstractAction.js @@ -0,0 +1,55 @@ +const abstractAction = Ext.extend(Ext.Action, { + + + /** + * @param config + * + * maskMsg: 'Please Wait...', + * documentType: '', // one of Offer|Order|Delivery|Invoice + * + */ + constructor: function (config) { + config.app = Tine.Tinebase.appMgr.get('Sales') + config.recordClass = Tine.Tinebase.data.RecordMgr.get(`Sales.Document_${config.documentType}`) + config.statusFieldName = `${config.documentType.toLowerCase()}_status` + config.statusDef = Tine.Tinebase.widgets.keyfield.getDefinitionFromMC(config.recordClass, config.statusFieldName) + + Ext.Action.prototype.constructor.call(this, config); + }, + // NOTE: action updater is not executed in action but in component of the action + // so it does not work to define it here + // actionUpdater(action, grants, records, isFilterSelect, filteredContainers) { + // let enabled = records.length === 1 // no batch processing yet, needs a robust concept! + // action.setDisabled(!enabled) + // action.baseAction.setDisabled(!enabled) // WTF? + // }, + handler: async function(cmp) { + // @TODO working with this might be a bad idea as it's excecuted here only and not in constructor? + + + // this.recordsName = recordClass.getRecordsName() + this.selections = [...this.initialConfig.selections] + this.errorMsgs = [] + this.editDialog = cmp.findParentBy((c) => {return c instanceof Tine.widgets.dialog.EditDialog}) + this.maskEl = cmp.findParentBy((c) => {return c instanceof Tine.widgets.dialog.EditDialog || c instanceof Tine.widgets.MainScreen }).getEl() + this.mask = new Ext.LoadMask(this.maskEl, { msg: this.maskMsg || this.app.i18n._('Please wait...') }) + + this.unbooked = this.selections.reduce((unbooked, record) => { + record.noProxy = true // kill grid autoSave + const status = record.get(this.statusFieldName) + return unbooked.concat(this.statusDef.records.find((r) => { return r.id === status })?.booked ? [] : [record]) + }, []) + + // if (editDialog) { + // try { + // await editDialog.isValid() + // } catch (e) { + // return + // } + // } + // + // this.handle(options) + } +}); + +export default abstractAction \ No newline at end of file diff --git a/tine20/Sales/js/Document/BookDocumentAction.js b/tine20/Sales/js/Document/BookDocumentAction.js new file mode 100644 index 00000000000..0887c7de007 --- /dev/null +++ b/tine20/Sales/js/Document/BookDocumentAction.js @@ -0,0 +1,96 @@ +/* + * Tine 2.0 + * + * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 + * @author Cornelius Weiss + * @copyright Copyright (c) 2022 Metaways Infosystems GmbH (http://www.metaways.de) + */ +import AbstractAction from "./AbstractAction"; + +Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), + Tine.Tinebase.ApplicationStarter.isInitialised()]).then(() => { + const app = Tine.Tinebase.appMgr.get('Sales') + + const getAction = (type, config) => { + return new AbstractAction({ + documentType: type, + text: app.i18n._('Book Document'), + iconCls: `action_book_document`, + actionUpdater(action, grants, records, isFilterSelect, filteredContainers) { + let enabled = records.length + + enabled = records.reduce((enabled, record) => { + return enabled && !_.find(action.statusDef.records, {id: record.get(action.statusFieldName) })?.booked + }, enabled) + + // action.setDisabled(!enabled) // this is the component itsef + action.baseAction.setDisabled(!enabled) // this is the action which sets all instances + }, + handler: async function(cmp) { + AbstractAction.prototype.handler.call(this, cmp); + + // @TODO: maybe we should define default booked state somehow? e.g. offer should be accepted (not only send) or let the user select? + const bookedState = this.statusDef.records.find((r) => { return r.booked }) + this.mask.show() + + try { + // check if date is set and ask if user want's to change it to today + const notToday = _.reduce(this.unbooked, (acc, record) => { + return _.concat(acc, record.get('date') && record.get('date').format('Ymd') !== new Date().format('Ymd') ? record : []); + }, []) + if (notToday.length) { + _.each(await Tine.widgets.dialog.MultiOptionsDialog.getOption({ + title: this.app.formatMessage('Change Document Date?'), + questionText: this.app.formatMessage('Please select the { sourceRecordsName } where you want to change the document date to today.', { sourceRecordsName: this.recordClass.getRecordsName() }), + allowMultiple: true, + allowEmpty: true, + allowCancel: false, + height: notToday.length * 30 + 100, + options: notToday.map((source) => { + return { text: source.getTitle() + ': ' + Tine.Tinebase.common.dateRenderer(source.get('date')), name: source.id, checked: false, source } + }) + }), (option) => { _.find(unbooked, { id: option.name }).set('date', new Date().clearTime()); debugger}); + } + } catch (e) {/* USERABORT -> continue */ } + + await this.unbooked.asyncForEach(async (record) => { + if (record.phantom) { + record = await this.recordClass.getProxy().promiseSaveRecord(record) + if (this.recordClass === this.editDialog?.recordClass) { + this.editDialog ? await this.editDialog.loadRecord(record) : null + } + } + record.set(this.statusFieldName, bookedState.id) + let updatedRecord + try { + updatedRecord = await this.recordClass.getProxy().promiseSaveRecord(record) + this.selections.splice.apply(this.selections, [this.selections.indexOf(record), 1].concat(updatedRecord ? [updatedRecord] : [])) + if (this.recordClass === this.editDialog?.recordClass) { + this.editDialog ? await this.editDialog.loadRecord(updatedRecord) : null + } + } catch (e) { + record.reject() + this.errorMsgs.push(this.app.formatMessage('Cannot book { sourceDocument }: ({e.code}) { e.message }', { sourceDocument: record.getTitle(), e })) + } + }) + this.mask.hide() + + if (this.errorMsgs.length) { + await Ext.MessageBox.show({ + buttons: Ext.Msg.OK, + icon: Ext.MessageBox.WARNING, + title: this.app.formatMessage('There where Errors:'), + msg: this.errorMsgs.join('
') + }) + } + } + }) + } + ['Offer', 'Order', 'Delivery', 'Invoice'].forEach((type) => { + const action = getAction(type, {}) + const medBtnStyle = { scale: 'medium', rowspan: 2, iconAlign: 'top'} + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-GridPanel-ContextMenu`, action, 2) + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-editDialog-Toolbar`, Ext.apply(new Ext.Button(action), medBtnStyle), 30) + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-GridPanel-ActionToolbar-leftbtngrp`, Ext.apply(new Ext.Button(action), medBtnStyle), 30) + }) +}) \ No newline at end of file diff --git a/tine20/Sales/js/Document/CreateFollowUpAction.js b/tine20/Sales/js/Document/CreateFollowUpAction.js index 03afcd3d4c9..e74eb661aba 100644 --- a/tine20/Sales/js/Document/CreateFollowUpAction.js +++ b/tine20/Sales/js/Document/CreateFollowUpAction.js @@ -103,7 +103,7 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), options: notToday.map((source) => { return { text: source.getTitle() + ': ' + Tine.Tinebase.common.dateRenderer(source.get('date')), name: source.id, checked: false, source } }) - }), (option) => { _.find(unbooked, { id: option.name }).set('date', new Date().clearTime()); debugger}); + }), (option) => { _.find(unbooked, { id: option.name }).set('date', new Date().clearTime()) }); } catch (e) {/* USERABORT -> continue */ } await unbooked.asyncForEach(async (record) => { diff --git a/tine20/Sales/js/Document/CreatePaperSlipAction.js b/tine20/Sales/js/Document/CreatePaperSlipAction.js index 45a7409ca1c..bb09afb8c61 100644 --- a/tine20/Sales/js/Document/CreatePaperSlipAction.js +++ b/tine20/Sales/js/Document/CreatePaperSlipAction.js @@ -11,6 +11,61 @@ import getTwingEnv from "twingEnv"; // #endif +/** + * create paper slip of given document + * @param config + * maskEl + * record + * recordClass + * editDialog + * + * @returns {Promise} + */ +const createAttachedDocument = async (config) => { + const app = Tine.Tinebase.appMgr.get('Sales') + const win = config.win || window + const recordClass = config.recordClass || config.record.constructor + const recordName = recordClass.getRecordName() + const typeName = config.type === 'paperslip' ? app.formatMessage('Paper Slip') : app.formatMessage('eDocument') + const api = config.type === 'paperslip' ? Tine.Sales.createPaperSlip : Tine.Sales.createEDocument + let record + let attachedDocument + + const maskMsg = app.formatMessage('Creating { recordName } { typeName }', { recordName, typeName }) + const mask = new win.Ext.LoadMask(config.maskEl, { msg: maskMsg }) + mask.show() + + try { + record = !config.editDialog ? config.record : + (config.editDialog.record.isModified() ? Tine.Tinebase.data.Record.setFromJson(await config.editDialog.applyChanges(), recordClass) : config.editDialog.record) + attachedDocument = record.getAttachedDocument(config.type) + if (!attachedDocument || config.force) { + record = Tine.Tinebase.data.Record.setFromJson(await api(recordClass.getPhpClassName(), config.record.id), recordClass) + config.editDialog ? await config.editDialog.loadRecord(record) : null + window.postal.publish({ + channel: "recordchange", + topic: [app.appName, recordClass.getMeta('modelName'), 'update'].join('.'), + data: {...record.data} + }); + attachedDocument = record.getAttachedDocument(config.type) + } + + + } catch (e) { + + await win.Ext.MessageBox.show({ + buttons: Ext.Msg.OK, + icon: Ext.MessageBox.WARNING, + title: app.formatMessage('There where Errors:'), + msg: app.formatMessage('Cannot create { typeName } for { recordName }: { title } ({e.code}) { e.message }', { recordName, typeName, title: config.record.getTitle(), e }) + }); + } + + mask.hide() + + return { record, attachedDocument } +}; + Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), Tine.Tinebase.ApplicationStarter.isInitialised()]).then(() => { const app = Tine.Tinebase.appMgr.get('Sales') @@ -25,12 +80,9 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), action.setDisabled(!enabled) action.baseAction.setDisabled(!enabled) // WTF? }, - async handler(cmp) { + async handler(cmp, e) { let record = this.initialConfig.selections[0]; - let paperSlip; // @see contentPanelConstructorInterceptor - const editDialog = cmp.findParentBy((c) => {return c instanceof Tine.widgets.dialog.EditDialog}) - const maskMsg = app.formatMessage('Creating {type} Paper Slip', { type: recordClass.getRecordName() }) if (editDialog) { try { @@ -40,29 +92,7 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), } } - const createPaperSlip = async (mask) => { - try { - mask.show() - record = editDialog ? Tine.Tinebase.data.Record.setFromJson(await editDialog.applyChanges(), recordClass) : record - record = Tine.Tinebase.data.Record.setFromJson(await Tine.Sales.createPaperSlip(recordClass.getPhpClassName(), record.id), recordClass) - editDialog ? await editDialog.loadRecord(record) : null - window.postal.publish({ - channel: "recordchange", - topic: [app.appName, recordClass.getMeta('modelName'), 'update'].join('.'), - data: {...record.data} - }); - } catch (e) { - await Ext.MessageBox.show({ - buttons: Ext.Msg.OK, - icon: Ext.MessageBox.WARNING, - title: app.formatMessage('There where Errors:'), - msg: app.formatMessage('Cannot create { type } paper slip: ({e.code}) { e.message }', { type: record.getTitle(), e }) - }); - } - mask.hide() - }; - - const getMailAction = async (win, record) => { + const getMailAction = async (win, record, paperSlip) => { const recipientData = _.get(record, 'data.recipient_id.data', _.get(record, 'data.recipient_id')) || {}; paperSlip.attachment_type = 'attachment'; @@ -122,6 +152,8 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), }); }; + const paperSlipConfig = { record, recordClass, editDialog, type: 'paperslip' } + paperSlipConfig.force = e.ctrlKey || e.altKey if (Tine.OnlyOfficeIntegrator) { Tine.OnlyOfficeIntegrator.OnlyOfficeEditDialog.openWindow({ id: record.id, @@ -131,22 +163,17 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), const mainCardPanel = isPopupWindow ? win.Tine.Tinebase.viewport.tineViewportMaincardpanel : await config.window.afterIsRendered() isPopupWindow ? mainCardPanel.get(0).hide() : null; - const mask = new win.Ext.LoadMask(mainCardPanel.el, { msg: maskMsg }) - await createPaperSlip(mask) - const attachments = record.get('attachments') - const mdates = _.map(attachments, (attachment) => {return _.compact([attachment.last_modified_time, attachment.creation_time]).sort().pop()}) - paperSlip = attachments[mdates.indexOf([...mdates].sort().pop())] + const {record, attachedDocument } = await createAttachedDocument(Object.assign(paperSlipConfig, { win, maskEl: mainCardPanel.el })) Object.assign(config, { - recordData: paperSlip, - id: paperSlip.id, - tbarItems: [await getMailAction(win, record)] + recordData: attachedDocument, + id: attachedDocument.id, + // tbarItems: [await getMailAction(win, record, paperSlip)] }); } }) } else { const maskEl = cmp.findParentBy((c) => {return c instanceof Tine.widgets.dialog.EditDialog || c instanceof Tine.widgets.MainScreen }).getEl() - const mask = new Ext.LoadMask(maskEl, { msg: maskMsg }) - await createPaperSlip(mask) + await createAttachedDocument(Object.assign(paperSlipConfig, { maskEl })) alert('OnlyOfficeIntegrator missing -> find paperSlip in attachments') } } @@ -160,3 +187,7 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-editDialog-Toolbar`, Ext.apply(new Ext.Button(action), medBtnStyle), 10) }) }) + +export { + createAttachedDocument +} \ No newline at end of file diff --git a/tine20/Sales/js/Document/DispatchDocumentAction.js b/tine20/Sales/js/Document/DispatchDocumentAction.js new file mode 100644 index 00000000000..925b93772c2 --- /dev/null +++ b/tine20/Sales/js/Document/DispatchDocumentAction.js @@ -0,0 +1,233 @@ +/* + * Tine 2.0 + * + * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 + * @author Cornelius Weiss + * @copyright Copyright (c) 2022 Metaways Infosystems GmbH (http://www.metaways.de) + */ + +// @see https://github.com/ericmorand/twing/issues/332 +// #if process.env.NODE_ENV !== 'unittest' +import getTwingEnv from "twingEnv"; +// #endif + +import AbstractAction from "./AbstractAction"; +import { createAttachedDocument } from "./CreatePaperSlipAction"; + +const setDispatched = async function(config) { + const app = Tine.Tinebase.appMgr.get('Sales') + const win = config.win || window + const recordClass = config.recordClass || config.record.constructor + const recordName = recordClass.getRecordName() + + const maskMsg = app.formatMessage('Set { recordName } dispatched', { recordName }) + const mask = new win.Ext.LoadMask(config.maskEl, { msg: maskMsg }) + mask.show() + + const docType = config.record.constructor.getMeta('recordName') + const statusFieldName = `${docType.toLowerCase()}_status` + const currentStatus = config.record.get(statusFieldName) + + let changeStatusTo = null; + if (docType === 'Invoice' && currentStatus === 'BOOKED') { + changeStatusTo = 'SHIPPED'; + } else if (docType === 'Offer' && currentStatus === 'DRAFT') { + // don't change status - might still be a draft! + } + if (changeStatusTo) { + config.record.set(statusFieldName, changeStatusTo) + if (config.editDialog) { + config.editDialog.getForm().findField(statusFieldName).setValue(changeStatusTo) + await config.editDialog.applyChanges() + config.record = config.editDialog.record + } else { + config.record = await config.record.getProxy().promiseSaveRecord(config.record) + } + } + + // set attached documents dispatched + const promises = _.map(config.docs, (doc) => { + const attachedDocument = _.find(config.record.get('attached_documents'), { node_id: doc.file.id }); + attachedDocument.dispatch_history = attachedDocument.dispatch_history || []; + attachedDocument.dispatch_history.push({ + attached_document_id: attachedDocument.id, + dispatch_date: new Date(), + dispatch_transport: config.dispatch_transport, + dispatch_report: config.dispatch_report + }) + return Tine.Sales.saveDocument_AttachedDocument(attachedDocument) + }) + _.each(await Promise.allSettled(promises), doc => { + // let's reload document + }) + + if (config.editDialog) { + config.editDialog.loadRecord('remote') + } else { + config.record = await config.record.getProxy().promiseLoadRecord(config.record) + } + mask.hide() +} + +Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), + Tine.Tinebase.ApplicationStarter.isInitialised()]).then(() => { + const app = Tine.Tinebase.appMgr.get('Sales') + + const getAction = (type, config) => { + return new AbstractAction({ + documentType: type, + text: config.text || app.formatMessage('Dispatch Document'), + iconCls: `action_dispatch_document`, + actionUpdater(action, grants, records, isFilterSelect, filteredContainers) { + let enabled = records.length + + enabled = records.reduce((enabled, record) => { + return enabled && _.find(action.statusDef.records, {id: record.get(action.statusFieldName) })?.booked + }, enabled) + + action.baseAction.setDisabled(!enabled) // this is the action which sets all instances + }, + handler: async function(cmp) { + AbstractAction.prototype.handler.call(this, cmp); + + // let record = [...this.initialConfig.selections][0] + // let paperSlip + + let record = this.selections = [...this.initialConfig.selections][0] + const transport = record.get('debitor_id').edocument_transport || 'manual' + const win = window + + + // autocheck paperslip, ubl and supporting_documents, offer all other attachments + let paperslip = record.getAttachedDocument('paperslip') + let edocument = record.getAttachedDocument('ubl') + let docs = _.concat([ + { name: 'paperslip', text: app.formatMessage('Paperslip ({ filename })', {filename: paperslip ? paperslip.name : app.formatMessage('Generated when dispatched')}), file: paperslip, checked: true }, + { name: 'ubl', text: app.formatMessage('eDocument ({ filename })', {filename: edocument ? edocument.name : app.formatMessage('Generated when dispatched')}), file: edocument, checked: true } + ], _.reduce(record.get('attachments'), (docs, attachment) => { + const attachedDocument = _.find(record.get('attached_documents'), { node_id: attachment.id }) + if (! attachedDocument || attachedDocument.type === 'supporting_document') { + docs.push({ name: attachment.id, text: attachment.name, file: attachment, checked: !!attachedDocument}) + } + return docs + }, [])) + + try { + docs = await Tine.widgets.dialog.MultiOptionsDialog.getOption({ + title: app.formatMessage('Please Select Files to Dispatch'), + questionText: app.formatMessage('Please select the files which should be dispatched.'), + allowMultiple: true, + allowEmpty: false, + allowCancel: true, + height: docs.length * 30 + 100, + options: docs + }) + } catch (e) {/* USERABORT */ return } + + this.mask.show() + + // create paperslip/ubl if nessesary + let promises = []; + if (_.find(docs, { name: 'paperslip' }) && !paperslip) { + promises.push(createAttachedDocument({ + record, + type: 'paperslip', + maskEl: this.maskEl, + editDialog: this.editDialog + }).then( ret => { + _.find(docs, { name: 'paperslip' }).file = ret.attachedDocument + })) + } + if (_.find(docs, { name: 'ubl' }) && !edocument) { + promises.push(createAttachedDocument({ + record, + type: 'ubl', + maskEl: this.maskEl, + editDialog: this.editDialog + }).then( ret => { + _.find(docs, { name: 'ubl' }).file = ret.attachedDocument + })) + } + + const dispatchedConfig = { + maskEl: this.maskEl, + editDialog: this.editDialog, + docs, + record, + transport, + win + } + + if (transport === 'email') { + win.Tine.Felamimail.MessageEditDialog.openWindow({ + contentPanelConstructorInterceptor: async (config) => { + await Promise.allSettled(promises) + const recipientData = _.get(record, 'data.recipient_id.data', _.get(record, 'data.recipient_id')) || {}; + const mailDefaults = win.Tine.Felamimail.Model.Message.getDefaultData() + const emailBoilerplate = _.find(record.get('boilerplates'), (bp) => { return bp.name === 'Email'}) + let body = '' + if (emailBoilerplate) { + this.twingEnv = getTwingEnv() + const loader = this.twingEnv.getLoader() + loader.setTemplate(`${record.id}-email`, emailBoilerplate.boilerplate) + body = await this.twingEnv.render(`${record.id}-email`, record.data) + if (mailDefaults.content_type === 'text/html') { + body = Ext.util.Format.nl2br(body) + } + } + + const mailRecord = new win.Tine.Felamimail.Model.Message(Object.assign(mailDefaults, { + subject: `${record.constructor.getRecordName()} ${record.get('document_number')}` + (record.get('document_title') ? `: ${record.get('document_title')}` : ''), + body: body, + to: [`${recipientData.name} < ${recipientData.email} >`], + attachments: _.map(docs, (doc) => { + return Object.assign(doc.file, { attachment_type: 'attachment' }) + }) + }), 0) + + Object.assign(config, { + record: mailRecord + }) + }, + listeners: { + update: (mail) => { + setDispatched(Object.assign(dispatchedConfig, { + report: mail // mhh, we could save the mail as attachment?! + // e.g. by fileTo tempfile?... no api's yet :-( + })) + } + } + }); + } else { + // @TODO download transport + await Promise.allSettled(promises) + await Ext.MessageBox.alert( + app.formatMessage('Manual Dispatch'), + app.formatMessage('Dispatch documents where created, please download and dispatch manually.') + ) + setDispatched(dispatchedConfig) + } + + this.mask.hide() + + if (this.errorMsgs.length) { + await Ext.MessageBox.show({ + buttons: Ext.Msg.OK, + icon: Ext.MessageBox.WARNING, + title: this.app.formatMessage('There where Errors:'), + msg: this.errorMsgs.join('
') + }) + } + } + }) + } + + + ['Invoice'].forEach((type) => { + const action = getAction(type, {}) + const medBtnStyle = { scale: 'medium', rowspan: 2, iconAlign: 'top'} + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-GridPanel-ContextMenu`, action, 2) + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-editDialog-Toolbar`, Ext.apply(new Ext.Button(action), medBtnStyle), 50) + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-GridPanel-ActionToolbar-leftbtngrp`, Ext.apply(new Ext.Button(action), medBtnStyle), 30) + }) +}) \ No newline at end of file diff --git a/tine20/Sales/js/Document/TracAction.js b/tine20/Sales/js/Document/TracAction.js index fcfc29221b6..3706cc17425 100644 --- a/tine20/Sales/js/Document/TracAction.js +++ b/tine20/Sales/js/Document/TracAction.js @@ -38,7 +38,7 @@ Promise.all([Tine.Tinebase.appMgr.isInitialised('Sales'), const medBtnStyle = { scale: 'medium', rowspan: 2, iconAlign: 'top'} Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-GridPanel-ContextMenu`, action, 2) Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-GridPanel-ActionToolbar-leftbtngrp`, Ext.apply(new Ext.Button(action), medBtnStyle), 30) - Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-editDialog-Toolbar`, Ext.apply(new Ext.Button(action), medBtnStyle), 10) + Ext.ux.ItemRegistry.registerItem(`Sales-Document_${type}-editDialog-Toolbar`, Ext.apply(new Ext.Button(action), medBtnStyle), 50) }) }) diff --git a/tine20/Sales/js/EDocument/QuickLookPanel.js b/tine20/Sales/js/EDocument/QuickLookPanel.js new file mode 100644 index 00000000000..e1f31829e58 --- /dev/null +++ b/tine20/Sales/js/EDocument/QuickLookPanel.js @@ -0,0 +1,28 @@ +/** + * Tine 2.0 + * + * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 + * @author Cornelius Weiß + * @copyright Copyright (c) 2024 Metaways Infosystems GmbH (http://www.metaways.de) + */ + +const EDocumentQuickLookPanel = Ext.extend(Ext.Panel, { + border: false, + + initComponent: function() { + this.html = `