From a0aded51d0959e770d2f418a861e1986c14fd5d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:26:31 +0000 Subject: [PATCH 1/2] Add an Excel import into the travel business object Z2UI5_CL_SMPS_APP_028 reads an .xlsx file with XCO, converts and checks every row in a preview, and creates all ready rows with one MODIFY ENTITIES. The %cid is the Excel row number, so refusals from the CREATE and from the validations at COMMIT (mapped back via MAPPED) are shown on the row they belong to. A COMMIT is all or nothing; "Skip refused rows" rolls back and creates the rest in a second attempt. The sample uses Z2UI5_R_SMPS_TRV, so package 13 now carries src/03 (and src/00, which 03 needs) on its branch. Catalogue, SAMPLES.md and the overview app list the new sample. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014YZFmDjwUy369k1iZKoBT5 --- .github/packages.json | 4 +- README.md | 2 +- SAMPLES.md | 3 +- catalogue-derived.json | 3 +- catalogue.json | 29 +- src/13/README.md | 28 ++ src/13/z2ui5_cl_smps_app_028.clas.abap | 555 +++++++++++++++++++++++++ src/13/z2ui5_cl_smps_app_028.clas.xml | 16 + src/z2ui5_cl_smps_app_000.clas.abap | 6 +- 9 files changed, 639 insertions(+), 7 deletions(-) create mode 100644 src/13/z2ui5_cl_smps_app_028.clas.abap create mode 100644 src/13/z2ui5_cl_smps_app_028.clas.xml diff --git a/.github/packages.json b/.github/packages.json index b390ab5..7c46d97 100644 --- a/.github/packages.json +++ b/.github/packages.json @@ -105,8 +105,8 @@ "title": "Excel & E-Mail", "runsOn": "Cloud + Standard ≥ 7.58 (2023)", "syntax": "v757", - "note": "The XCO XLSX library and cl_bcs_mail_message are released ABAP Cloud APIs; on-premise the package needs a release that carries both. The mail sample also needs outbound mail configured.", - "shared": [] + "note": "The XCO XLSX library and cl_bcs_mail_message are released ABAP Cloud APIs; on-premise the package needs a release that carries both. The mail sample also needs outbound mail configured. The import sample creates travels in the business object of package 03, so the branch carries src/03 along.", + "shared": ["00", "03"] }, { "dir": "14", diff --git a/README.md b/README.md index b8d3f33..3bee620 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ package, and skip the rest: | Call a large language model from your app — a chat, or a table summarized by AI | [`src/10`](src/10/README.md) — AI / LLM | an HTTPS destination to an LLM endpoint (SM59 or BTP), or SAP's ABAP AI SDK | | Upload a file into a RAP business object through an action with a deep parameter | [`src/11`](src/11/README.md) — RAP File Upload | ≥ 2025, the release that carries deep action parameters with `@Semantics.largeObject`; the business object ships with the package | | Ask a business object what is allowed, call actions with parameters, factory actions and default values, validate while the user types | [`src/12`](src/12/README.md) — RAP Actions & Permissions | ≥ 2025; the business object ships with the package | -| Read an uploaded Excel file, export a table as Excel, send it as a mail attachment | [`src/13`](src/13/README.md) — Excel & E-Mail | ≥ 2023 or ABAP Cloud; outbound mail configured for the mail sample | +| Read an uploaded Excel file, import it into a RAP business object, export a table as Excel, send it as a mail attachment | [`src/13`](src/13/README.md) — Excel & E-Mail | ≥ 2023 or ABAP Cloud; outbound mail configured for the mail sample | | Render Adobe Forms to PDF and download one or several as a ZIP | [`src/14`](src/14/README.md) — Adobe Forms | on-premise; Adobe Document Services configured | Each package README opens with a **What you need** section that turns the last diff --git a/SAMPLES.md b/SAMPLES.md index 04f7eab..604ddd9 100644 --- a/SAMPLES.md +++ b/SAMPLES.md @@ -3,7 +3,7 @@ # The sample catalogue -Every app in this repository — 47 of them — with what it shows and a +Every app in this repository — 48 of them — with what it shows and a link to its source. This is the [overview app](src/z2ui5_cl_smps_app_000.clas.abap) as a page you can read here, before installing anything. @@ -141,6 +141,7 @@ type who does not know it exists. `Ctrl+F` on this page uses them. | **Excel** — Read an Uploaded XLSX File with XCO
An .xlsx file from the FileUploader read with the XCO XLSX library - the first worksheet becomes an ABAP table and is shown as it arrived
excel xlsx upload import read spreadsheet xco fileuploader table | [`Z2UI5_CL_SMPS_APP_021`](src/13/z2ui5_cl_smps_app_021.clas.abap) | | **Excel** — Export a Table as XLSX File with XCO
The table on the screen written into an .xlsx file with the XCO XLSX library and handed to the browser as a download - a header row, then one row per line
excel xlsx export download write spreadsheet xco table download_b64_file | [`Z2UI5_CL_SMPS_APP_022`](src/13/z2ui5_cl_smps_app_022.clas.abap) | | **E-Mail** — Send a Table as XLSX Attachment
A mail with an .xlsx attachment, sent with cl_bcs_mail_message - recipient, subject and text from the screen, the attachment written with the XCO XLSX library
email mail send attachment xlsx excel cl_bcs_mail_message smtp | [`Z2UI5_CL_SMPS_APP_023`](src/13/z2ui5_cl_smps_app_023.clas.abap) | +| **Excel** — Import Travels into a RAP Business Object
An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to
excel xlsx import upload mass create rap eml business object travel fileuploader | [`Z2UI5_CL_SMPS_APP_028`](src/13/z2ui5_cl_smps_app_028.clas.abap) | ### Adobe Forms — `src/14` diff --git a/catalogue-derived.json b/catalogue-derived.json index f0e90b5..42d3e0b 100644 --- a/catalogue-derived.json +++ b/catalogue-derived.json @@ -71,7 +71,7 @@ "sap.m.TextArea" ], "counts": { - "samples": 47, + "samples": 48, "controls": 61, "noChain": 0 }, @@ -104,6 +104,7 @@ {"class":"Z2UI5_CL_SMPS_APP_025","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,5,23,7,10,11,12,13,18,41,17,38],"controlCount":31}, {"class":"Z2UI5_CL_SMPS_APP_026","minUi5":"1.71","needs":[],"controls":[2,3,22,37,4,45,5,7,9,10,50,11,12,13,18,15,17,38],"controlCount":24}, {"class":"Z2UI5_CL_SMPS_APP_027","minUi5":"1.71","needs":[],"controls":[2,3,37,5,7,10,51,11,12,18,17],"controlCount":12}, + {"class":"Z2UI5_CL_SMPS_APP_028","minUi5":"1.71","needs":[],"controls":[1,52,2,3,31,5,6,7,10,11,12,13,18,41,17,56],"controlCount":38}, {"class":"Z2UI5_CL_SMPS_APP_313","minUi5":"1.71","needs":[],"controls":[7,10,19,20,21,17],"controlCount":6}, {"class":"Z2UI5_CL_SMPS_APP_314","minUi5":"1.71","needs":[],"controls":[2,3,22,7,10,11,12,13,18,17],"controlCount":31}, {"class":"Z2UI5_CL_SMPS_APP_315","minUi5":"1.71","needs":[],"controls":[2,3,7,10,11,12,13,18,17],"controlCount":35}, diff --git a/catalogue.json b/catalogue.json index 5ab7bb9..7f55941 100644 --- a/catalogue.json +++ b/catalogue.json @@ -169,7 +169,7 @@ "runsOn": "Cloud + Standard ≥ 7.58 (2023)", "cloud": true, "release": "7.58 (2023)", - "note": "The XCO XLSX library and cl_bcs_mail_message are released ABAP Cloud APIs; on-premise the package needs a release that carries both. The mail sample also needs outbound mail configured.", + "note": "The XCO XLSX library and cl_bcs_mail_message are released ABAP Cloud APIs; on-premise the package needs a release that carries both. The mail sample also needs outbound mail configured. The import sample creates travels in the business object of package 03, so the branch carries src/03 along.", "branch": "13-excel-email", "readme": "src/13/README.md" }, @@ -1234,6 +1234,33 @@ "branch": "13-excel-email", "setup": "src/13/README.md" }, + { + "class": "Z2UI5_CL_SMPS_APP_028", + "path": "src/13/z2ui5_cl_smps_app_028.clas.abap", + "package": "src/13", + "technology": "Excel & E-Mail", + "title": "Excel — Import Travels into a RAP Business Object", + "summary": "An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to", + "keywords": [ + "excel", + "xlsx", + "import", + "upload", + "mass", + "create", + "rap", + "eml", + "business", + "object", + "travel", + "fileuploader" + ], + "runsOn": "Cloud + Standard ≥ 7.58 (2023)", + "cloud": true, + "needs": "the XCO XLSX library; outbound mail for the mail sample", + "branch": "13-excel-email", + "setup": "src/13/README.md" + }, { "class": "Z2UI5_CL_SMPS_APP_024", "path": "src/14/z2ui5_cl_smps_app_024.clas.abap", diff --git a/src/13/README.md b/src/13/README.md index e2cbc81..a8d7ee0 100644 --- a/src/13/README.md +++ b/src/13/README.md @@ -22,6 +22,10 @@ The mail sample also needs **outbound mail** configured: on ABAP Cloud a communication arrangement for the scenario `SAP_COM_0548`, on-premise SMTP in transaction `SCOT`. Without it the send fails, and the sample shows the reason. +The import sample creates travels in the business object `Z2UI5_R_SMPS_TRV` of +[`03` RAP](../03/README.md). The branch carries `src/03` along for it, so +nothing has to be installed separately. + ## The samples | You want to | Sample | @@ -29,6 +33,7 @@ transaction `SCOT`. Without it the send fails, and the sample shows the reason. | read an uploaded `.xlsx` file into an ABAP table | [`021`](z2ui5_cl_smps_app_021.clas.abap) | | export a table as an `.xlsx` download | [`022`](z2ui5_cl_smps_app_022.clas.abap) | | send a table as an `.xlsx` mail attachment | [`023`](z2ui5_cl_smps_app_023.clas.abap) | +| import the rows of an `.xlsx` file into a RAP business object | [`028`](z2ui5_cl_smps_app_028.clas.abap) | The XCO calls sit in [`Z2UI5_CL_SMPS_XLSX`](z2ui5_cl_smps_xlsx.clas.abap), which the three samples share: an `.xlsx` in, a table of strings out, and the way @@ -55,6 +60,29 @@ Every cell travels as a string, so one table type fits every sheet; the app converts what it needs. Numbers are written with a decimal point and no thousands separator, the way a spreadsheet reads them back. +## Import into a business object + +`Z2UI5_CL_SMPS_APP_028` goes one step further than reading: the rows become +travels. Upload converts every cell into the type of its field and shows the +result, so a wrong date is seen before anything is created. Import sends all +rows in one `MODIFY ENTITIES ... CREATE`, with the Excel row number as `%cid`: + +```abap +MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + CREATE FIELDS ( agencyid customerid begindate enddate bookingfee currencycode description ) + WITH VALUE #( FOR t IN t_travel ( %cid = |ROW_{ t-row }| agencyid = t-agency_id ... ) ) + MAPPED DATA(s_mapped) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). +``` + +The `%cid` is what brings each answer back to its row. A refusal of the CREATE +carries it directly. The validations run at `COMMIT ENTITIES`, and their +answers carry the key instead, which `MAPPED` translates back to the `%cid`. +A COMMIT saves everything or nothing; with *Skip refused rows* on, the app +rolls back and creates the remaining rows in a second attempt. + ## Where to go next - [`11` RAP File Upload](../11/README.md) — a file stored in a business object. diff --git a/src/13/z2ui5_cl_smps_app_028.clas.abap b/src/13/z2ui5_cl_smps_app_028.clas.abap new file mode 100644 index 0000000..25bc1c3 --- /dev/null +++ b/src/13/z2ui5_cl_smps_app_028.clas.abap @@ -0,0 +1,555 @@ +" @keywords excel xlsx import upload mass create rap eml business object travel fileuploader +" @summary An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to +"!

Excel - Import Travels into a RAP Business Object

+"! An Excel import in three steps, each one a roundtrip: +"! +"! 1. Upload - the FileUploader brings the file, the XCO XLSX library reads +"! it (z2ui5_cl_smps_xlsx), and every row is converted and checked in the +"! app. Nothing touches the business object yet. +"! 2. Preview - the table shows each row with its status, so a wrong date or +"! an amount that is no number is seen before anything is created. +"! 3. Import - one MODIFY ENTITIES creates all rows at once. The %cid of an +"! instance is the Excel row number, so every message the business object +"! sends back - from the CREATE or from the validations at COMMIT - is +"! shown on the row it belongs to: +"! +"! MODIFY ENTITIES OF z2ui5_r_smps_trv +"! ENTITY travel +"! CREATE FIELDS ( agencyid customerid begindate enddate ... ) +"! WITH VALUE #( FOR r IN rows ( %cid = |ROW_{ r-row }| ... ) ) +"! MAPPED DATA(s_mapped) FAILED DATA(s_failed) REPORTED DATA(s_reported). +"! +"! COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv +"! FAILED DATA(s_failed_commit) REPORTED DATA(s_reported_commit). +"! +"! A COMMIT is all or nothing: one refused row and no travel is saved. With +"! "Skip refused rows" the app rolls back, drops the refused rows and creates +"! the rest in a second attempt. +"! +"! Needs the travel business object z2ui5_r_smps_trv of package 03, which +"! comes with this package's branch. Download Example File writes a sheet +"! with one row the business object refuses. +CLASS z2ui5_cl_smps_app_028 DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_row, + "! the row number in the sheet - the first data row is 2 + row TYPE i, + agency_id TYPE string, + customer_id TYPE string, + begin_date TYPE string, + end_date TYPE string, + booking_fee TYPE string, + currency TYPE string, + "! None, Success, Warning or Error - the sap.ui.core.ValueState + state TYPE string, + status TYPE string, + travel_id TYPE string, + END OF ty_s_row. + TYPES ty_t_row TYPE STANDARD TABLE OF ty_s_row WITH EMPTY KEY. + + "! the picked file - a data URL, data:<mime>;base64,<payload> + DATA file TYPE string. + DATA file_name TYPE string. + DATA t_rows TYPE ty_t_row. + DATA skip_refused TYPE abap_bool. + + PROTECTED SECTION. + + TYPES: + BEGIN OF ty_s_travel, + row TYPE i, + agency_id TYPE z2ui5_t_smps_trv-agency_id, + customer_id TYPE z2ui5_t_smps_trv-customer_id, + begin_date TYPE d, + end_date TYPE d, + booking_fee TYPE z2ui5_t_smps_trv-booking_fee, + currency TYPE z2ui5_t_smps_trv-currency_code, + END OF ty_s_travel. + TYPES ty_t_travel TYPE STANDARD TABLE OF ty_s_travel WITH EMPTY KEY. + + DATA client TYPE REF TO z2ui5_if_client. + + METHODS on_event. + METHODS file_read. + METHODS rows_import. + METHODS example_download. + METHODS view_display. + + "! one row of the sheet into the types of the business object - the + "! problems found go into the row's status, which stays Error unless + "! travel is complete + METHODS row_convert + EXPORTING + travel TYPE ty_s_travel + CHANGING + row TYPE ty_s_row. + + "! a date cell - the serial number Excel stores, or a text 2026-12-31 + METHODS date_convert + IMPORTING + val TYPE string + RETURNING + VALUE(result) TYPE d. + + "! one attempt: CREATE all travels, COMMIT, and on any refusal ROLLBACK - + "! the refused rows are marked in t_rows and returned + METHODS travels_create + IMPORTING + t_travel TYPE ty_t_travel + RETURNING + VALUE(result) TYPE ty_t_travel. + + METHODS row_refuse + IMPORTING + row TYPE i + text TYPE string. + + PRIVATE SECTION. +ENDCLASS. + + +CLASS z2ui5_cl_smps_app_028 IMPLEMENTATION. + + METHOD z2ui5_if_app~main. + + me->client = client. + IF client->check_on_navigated( ). + view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). + ENDIF. + + ENDMETHOD. + + + METHOD on_event. + + CASE client->get_event( ). + WHEN `UPLOAD`. + file_read( ). + WHEN `IMPORT`. + rows_import( ). + WHEN `EXAMPLE`. + example_download( ). + ENDCASE. + + ENDMETHOD. + + + METHOD file_read. + + " data:;base64, - only the payload is the file + DATA(base64) = substring_after( val = file sub = `,` ). + " the file is in t_rows now - kept in the app it would travel along with + " every roundtrip from here on + CLEAR file. + CLEAR t_rows. + + TRY. + DATA(t_cells) = z2ui5_cl_smps_xlsx=>read( cl_web_http_utility=>decode_x_base64( base64 ) ). + CATCH cx_root INTO DATA(error). + " not an .xlsx at all, or a damaged one - XCO says why + client->message_box_display( text = |{ file_name }: { error->get_text( ) }| type = `error` ). + view_display( ). + RETURN. + ENDTRY. + + " row 1 is the header; a row without any value is the end of the data + " for a person, not an error + LOOP AT t_cells INTO DATA(cells) FROM 2. + IF cells IS INITIAL. + CONTINUE. + ENDIF. + DATA(row) = VALUE ty_s_row( row = sy-tabix + agency_id = condense( cells-a ) + customer_id = condense( cells-b ) + begin_date = condense( cells-c ) + end_date = condense( cells-d ) + booking_fee = condense( cells-e ) + currency = to_upper( condense( cells-f ) ) ). + row_convert( CHANGING row = row ). + APPEND row TO t_rows. + ENDLOOP. + + DATA(errors) = REDUCE i( INIT n = 0 FOR r IN t_rows WHERE ( state = `Error` ) NEXT n = n + 1 ). + client->message_toast_display( |{ file_name } - { lines( t_rows ) } rows read, { errors } with errors| ). + view_display( ). + + ENDMETHOD. + + + METHOD row_convert. + + " the checks an import can make without the business object: does every + " cell fit the type of its field. What a travel must satisfy beyond that + " - a customer, an end date not before the begin date - is the business + " object's to decide, and it does at the COMMIT + travel = VALUE #( row = row-row ). + DATA(problems) = VALUE string_table( ). + + IF matches( val = row-agency_id regex = `^\d{1,6}$` ). + travel-agency_id = row-agency_id. + ELSE. + APPEND |agency '{ row-agency_id }' is not a number of up to 6 digits| TO problems. + ENDIF. + + " an empty customer is passed on on purpose - the validation + " validateCustomer of the business object answers it + IF row-customer_id IS INITIAL OR matches( val = row-customer_id regex = `^\d{1,6}$` ). + travel-customer_id = row-customer_id. + ELSE. + APPEND |customer '{ row-customer_id }' is not a number of up to 6 digits| TO problems. + ENDIF. + + travel-begin_date = date_convert( row-begin_date ). + IF travel-begin_date IS INITIAL. + APPEND |begin date '{ row-begin_date }' is no date| TO problems. + ENDIF. + + travel-end_date = date_convert( row-end_date ). + IF travel-end_date IS INITIAL. + APPEND |end date '{ row-end_date }' is no date| TO problems. + ENDIF. + + TRY. + travel-booking_fee = row-booking_fee. + CATCH cx_sy_conversion_error. + APPEND |booking fee '{ row-booking_fee }' is not a number - write it like 20.00| TO problems. + ENDTRY. + + travel-currency = row-currency. + + IF problems IS NOT INITIAL. + row-state = `Error`. + row-status = concat_lines_of( table = problems sep = `; ` ). + RETURN. + ENDIF. + + row-state = `None`. + row-status = `Ready`. + + ENDMETHOD. + + + METHOD date_convert. + + " the string value of a number cell may come with a fraction, 45658.0 + IF matches( val = val regex = `^\d{1,5}(\.0+)?$` ). + " a date cell: Excel stores the days since 1899-12-30 + result = CONV d( '18991230' ) + CONV i( val ). + ELSEIF matches( val = val regex = `^\d{4}-\d{2}-\d{2}$` ). + result = replace( val = val sub = `-` with = `` occ = 0 ). + ELSE. + RETURN. + ENDIF. + + " 2026-02-30 has the right shape and is still no date - a date that is + " not valid converts to the day number 0 + IF CONV i( result ) = 0. + CLEAR result. + ENDIF. + + ENDMETHOD. + + + METHOD rows_import. + + DATA t_travel TYPE ty_t_travel. + + LOOP AT t_rows REFERENCE INTO DATA(row) WHERE travel_id IS INITIAL. + row_convert( IMPORTING travel = DATA(travel) CHANGING row = row->* ). + IF row->state <> `Error`. + APPEND travel TO t_travel. + ENDIF. + ENDLOOP. + + IF t_travel IS INITIAL. + client->message_box_display( `Nothing to import - upload a file with at least one row without errors first` ). + view_display( ). + RETURN. + ENDIF. + + DATA(pending) = REDUCE i( INIT n = 0 FOR r IN t_rows WHERE ( travel_id IS INITIAL ) NEXT n = n + 1 ). + IF skip_refused = abap_false AND lines( t_travel ) < pending. + client->message_box_display( text = `Some rows have errors - fix the file, or switch on Skip refused rows` type = `error` ). + view_display( ). + RETURN. + ENDIF. + + DATA(created_before) = REDUCE i( INIT n = 0 FOR r IN t_rows WHERE ( travel_id IS NOT INITIAL ) NEXT n = n + 1 ). + DATA(t_refused) = travels_create( t_travel ). + + " a COMMIT is all or nothing, so the rows that were fine are not saved + " either. Skipping means: the same CREATE once more, without the rows the + " business object refused + IF t_refused IS NOT INITIAL AND skip_refused = abap_true. + DATA(t_retry) = t_travel. + LOOP AT t_refused INTO DATA(refused). + DELETE t_retry WHERE row = refused-row. + ENDLOOP. + IF t_retry IS NOT INITIAL. + LOOP AT t_retry INTO DATA(retry). + READ TABLE t_rows ASSIGNING FIELD-SYMBOL() WITH KEY row = retry-row. + IF sy-subrc = 0. + -state = `None`. + -status = `Ready`. + ENDIF. + ENDLOOP. + t_refused = travels_create( t_retry ). + ENDIF. + ENDIF. + + DATA(created) = REDUCE i( INIT n = 0 FOR r IN t_rows WHERE ( travel_id IS NOT INITIAL ) NEXT n = n + 1 ) - created_before. + DATA(refused_rows) = REDUCE i( INIT n = 0 FOR r IN t_rows WHERE ( state = `Error` ) NEXT n = n + 1 ). + IF refused_rows = 0. + client->message_toast_display( |{ created } travels created| ). + ELSE. + client->message_box_display( text = |{ created } travels created, { refused_rows } rows not imported - see the status column| + type = COND #( WHEN created = 0 THEN `error` ELSE `warning` ) ). + ENDIF. + view_display( ). + + ENDMETHOD. + + + METHOD travels_create. + + DATA row TYPE i. + DATA texts TYPE string_table. + + " the %cid is the Excel row number: it is the one thing the business + " object hands back with each instance, before a key exists - so it is + " what leads every answer back to its row + MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + CREATE FIELDS ( agencyid customerid begindate enddate bookingfee currencycode description ) + WITH VALUE #( FOR t IN t_travel + ( %cid = |ROW_{ t-row }| + agencyid = t-agency_id + customerid = t-customer_id + begindate = t-begin_date + enddate = t-end_date + bookingfee = t-booking_fee + currencycode = t-currency + description = |{ file_name }, row { t-row }| ) ) + MAPPED DATA(s_mapped) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + + IF s_failed-travel IS NOT INITIAL. + + ROLLBACK ENTITIES. + LOOP AT s_failed-travel INTO DATA(failed). + row = substring_after( val = failed-%cid sub = `ROW_` ). + texts = VALUE #( FOR r IN s_reported-travel WHERE ( %cid = failed-%cid AND %msg IS BOUND ) + ( r-%msg->if_message~get_text( ) ) ). + row_refuse( row = row text = concat_lines_of( table = texts sep = `; ` ) ). + APPEND VALUE #( row = row ) TO result. + ENDLOOP. + + ELSE. + + " the validations run now. Their answers carry the key, not the %cid - + " MAPPED is what translates one into the other + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). + + IF s_failed_commit IS INITIAL. + LOOP AT s_mapped-travel INTO DATA(created). + row = substring_after( val = created-%cid sub = `ROW_` ). + READ TABLE t_rows ASSIGNING FIELD-SYMBOL() WITH KEY row = row. + IF sy-subrc = 0. + -travel_id = |{ created-travelid ALPHA = OUT }|. + -state = `Success`. + -status = `Created`. + ENDIF. + ENDLOOP. + RETURN. + ENDIF. + + " a failed save keeps the instances in the transactional buffer - + " discard them, or the next attempt and abap2UI5's own COMMIT WORK at + " the end of the roundtrip run into them + ROLLBACK ENTITIES. + LOOP AT s_failed_commit-travel INTO DATA(failed_commit). + DATA(mapped) = VALUE #( s_mapped-travel[ travelid = failed_commit-travelid ] OPTIONAL ). + row = substring_after( val = mapped-%cid sub = `ROW_` ). + texts = VALUE #( FOR r IN s_reported_commit-travel WHERE ( travelid = failed_commit-travelid AND %msg IS BOUND ) + ( r-%msg->if_message~get_text( ) ) ). + row_refuse( row = row text = concat_lines_of( table = texts sep = `; ` ) ). + APPEND VALUE #( row = row ) TO result. + ENDLOOP. + + ENDIF. + + " one refusal and nothing is saved - the rows the business object did + " not object to are rolled back with the others + LOOP AT t_travel INTO DATA(travel). + IF NOT line_exists( result[ row = travel-row ] ). + READ TABLE t_rows ASSIGNING WITH KEY row = travel-row. + IF sy-subrc = 0. + -state = `Warning`. + -status = `Not saved - another row was refused`. + ENDIF. + ENDIF. + ENDLOOP. + + ENDMETHOD. + + + METHOD row_refuse. + + READ TABLE t_rows ASSIGNING FIELD-SYMBOL() WITH KEY row = row. + IF sy-subrc <> 0. + RETURN. + ENDIF. + -state = `Error`. + " a business object may refuse without a message - saying so beats an + " empty cell + -status = COND #( WHEN text IS INITIAL THEN `Refused by the business object` ELSE text ). + + ENDMETHOD. + + + METHOD example_download. + + DATA(today) = cl_abap_context_info=>get_system_date( ). + DATA(t_cells) = VALUE z2ui5_cl_smps_xlsx=>ty_t_row( + ( a = `Agency` b = `Customer` c = `Begin Date` d = `End Date` e = `Booking Fee` f = `Currency` ) + ( a = `70001` b = `1` c = |{ CONV d( today + 30 ) DATE = ISO }| d = |{ CONV d( today + 37 ) DATE = ISO }| e = `20.00` f = `EUR` ) + ( a = `70002` b = `2` c = |{ CONV d( today + 45 ) DATE = ISO }| d = |{ CONV d( today + 49 ) DATE = ISO }| e = `35.50` f = `EUR` ) + " the business object refuses this one at the COMMIT: the end date + " is before the begin date + ( a = `70003` b = `3` c = |{ CONV d( today + 60 ) DATE = ISO }| d = |{ CONV d( today + 55 ) DATE = ISO }| e = `15.00` f = `USD` ) + ( a = `70001` b = `4` c = |{ CONV d( today + 90 ) DATE = ISO }| d = |{ CONV d( today + 97 ) DATE = ISO }| e = `40.00` f = `EUR` ) ). + + DATA(xlsx) = z2ui5_cl_smps_xlsx=>write( t_cells ). + + client->follow_up_action( + val = client->cs_event-download_b64_file + t_arg = VALUE #( ( |data:{ z2ui5_cl_smps_xlsx=>mime_type };base64,{ cl_web_http_utility=>encode_x_base64( xlsx ) }| ) + ( `travels.xlsx` ) ) ). + + ENDMETHOD. + + + METHOD view_display. + + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `View` ns = `mvc` + )->a( n = `displayBlock` v = `true` + )->a( n = `height` v = `100%` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` + )->a( n = `xmlns:z2ui5` v = `z2ui5.cc` ). + DATA(page) = view->ele( `Shell` + )->ele( `Page` + )->a( n = `title` v = `abap2UI5 - Excel - 28 Import Travels into a RAP Business Object` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + + page->tag( `MessageStrip` + )->a( n = `text` v = `Columns A to F: agency, customer, begin date, end date, booking fee, currency - row 1 is the header. ` && + `Upload checks every row, Import creates the travels in Z2UI5_R_SMPS_TRV. No file at hand? Download Example File first.` + )->a( n = `type` v = `Information` + )->a( n = `showIcon` b = abap_true + )->a( n = `class` v = `sapUiSmallMargin` ). + + page->ele( `HBox` + )->a( n = `class` v = `sapUiSmallMargin` + )->a( n = `alignItems` v = `Center` + )->tag( n = `FileUploader` ns = `z2ui5` + )->a( n = `placeholder` v = `pick an .xlsx file...` + )->a( n = `fileType` v = `xlsx` + )->a( n = `uploadButtonText` v = `Upload` + )->a( n = `upload` v = client->_event( `UPLOAD` ) + )->a( n = `path` v = client->_bind( file_name ) + )->a( n = `value` v = client->_bind( file ) + )->tag( `Button` + )->a( n = `press` v = client->_event( `EXAMPLE` ) + )->a( n = `text` v = `Download Example File` + )->a( n = `icon` v = `sap-icon://download` + )->a( n = `class` v = `sapUiSmallMarginBegin` ). + + DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_rows ) + )->a( n = `noDataText` v = `Nothing read yet` ). + table->ele( `headerToolbar` + )->ele( `Toolbar` + )->tag( `Title` + )->a( n = `text` v = `Preview` + )->tag( `ToolbarSpacer` + )->tag( `CheckBox` + )->a( n = `text` v = `Skip refused rows` + )->a( n = `selected` v = client->_bind( skip_refused ) + )->tag( `Button` + )->a( n = `press` v = client->_event( `IMPORT` ) + )->a( n = `text` v = `Import` + )->a( n = `type` v = `Emphasized` + )->a( n = `enabled` b = xsdbool( t_rows IS NOT INITIAL ) ). + + table->ele( `columns` + )->ele( `Column` + )->a( n = `width` v = `4rem` + )->tag( `Text` + )->a( n = `text` v = `Row` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Agency` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Customer` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Begin` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `End` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Fee` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Status` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Travel` ). + + table->ele( `items` + )->ele( `ColumnListItem` + )->ele( `cells` + )->tag( `Text` + )->a( n = `text` v = `{ROW}` + )->tag( `Text` + )->a( n = `text` v = `{AGENCY_ID}` + )->tag( `Text` + )->a( n = `text` v = `{CUSTOMER_ID}` + )->tag( `Text` + )->a( n = `text` v = `{BEGIN_DATE}` + )->tag( `Text` + )->a( n = `text` v = `{END_DATE}` + )->tag( `Text` + )->a( n = `text` v = `{BOOKING_FEE} {CURRENCY}` + )->tag( `ObjectStatus` + )->a( n = `text` v = `{STATUS}` + )->a( n = `state` v = `{STATE}` + )->tag( `Text` + )->a( n = `text` v = `{TRAVEL_ID}` ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/13/z2ui5_cl_smps_app_028.clas.xml b/src/13/z2ui5_cl_smps_app_028.clas.xml new file mode 100644 index 0000000..ce56969 --- /dev/null +++ b/src/13/z2ui5_cl_smps_app_028.clas.xml @@ -0,0 +1,16 @@ + + + + + + Z2UI5_CL_SMPS_APP_028 + E + Excel - Import Travels into a RAP Business Object + 1 + X + X + X + + + + diff --git a/src/z2ui5_cl_smps_app_000.clas.abap b/src/z2ui5_cl_smps_app_000.clas.abap index 4fd97cd..92c4ed1 100644 --- a/src/z2ui5_cl_smps_app_000.clas.abap +++ b/src/z2ui5_cl_smps_app_000.clas.abap @@ -1116,7 +1116,11 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. ( sample( no = `023` title = `Send a table as XLSX attachment` detail = `A mail with an .xlsx attachment, sent with cl_bcs_mail_message - recipient, subject and text from the screen, the attachment written with the XCO XLSX library` - classname = `Z2UI5_CL_SMPS_APP_023` ) ) ). + classname = `Z2UI5_CL_SMPS_APP_023` ) ) + ( sample( no = `028` + title = `Import an XLSX file into a RAP business object` + detail = `An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to` + classname = `Z2UI5_CL_SMPS_APP_028` ) ) ). t_forms = VALUE #( ( sample( no = `024` From e1a7c5adb2111d2f30a6777f6995bd52956b8968 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:38:44 +0000 Subject: [PATCH 2/2] Add three everyday RAP apps and a report with Excel export Each one is the worked example of a guide in the documentation's new Everyday Work section, on the travel business object of package 03: - Z2UI5_CL_SMPS_APP_029 mass change: describe the change once, preview old and new values per row, one MODIFY ... UPDATE for all, refusals from the UPDATE and the COMMIT shown on their row by key - Z2UI5_CL_SMPS_APP_030 approval worklist: the open travels, several accepted or rejected with one EXECUTE, RESULT for the new status - Z2UI5_CL_SMPS_APP_032 edit with conflict check: optimistic locking in a stateless app - LastChangedAt remembered on read and compared after the UPDATE has taken the lock, with a button that plays the second user - Z2UI5_CL_SMPS_APP_031 (package 13) report: selection as range tables, one SELECT, a total per currency, the shown rows exported with XCO Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014YZFmDjwUy369k1iZKoBT5 --- SAMPLES.md | 6 +- catalogue-derived.json | 43 +-- catalogue.json | 102 +++++++ src/03/README.md | 14 +- src/03/z2ui5_cl_smps_app_029.clas.abap | 394 +++++++++++++++++++++++++ src/03/z2ui5_cl_smps_app_029.clas.xml | 16 + src/03/z2ui5_cl_smps_app_030.clas.abap | 277 +++++++++++++++++ src/03/z2ui5_cl_smps_app_030.clas.xml | 16 + src/03/z2ui5_cl_smps_app_032.clas.abap | 390 ++++++++++++++++++++++++ src/03/z2ui5_cl_smps_app_032.clas.xml | 16 + src/13/README.md | 5 +- src/13/z2ui5_cl_smps_app_031.clas.abap | 330 +++++++++++++++++++++ src/13/z2ui5_cl_smps_app_031.clas.xml | 16 + src/z2ui5_cl_smps_app_000.clas.abap | 20 +- 14 files changed, 1619 insertions(+), 26 deletions(-) create mode 100644 src/03/z2ui5_cl_smps_app_029.clas.abap create mode 100644 src/03/z2ui5_cl_smps_app_029.clas.xml create mode 100644 src/03/z2ui5_cl_smps_app_030.clas.abap create mode 100644 src/03/z2ui5_cl_smps_app_030.clas.xml create mode 100644 src/03/z2ui5_cl_smps_app_032.clas.abap create mode 100644 src/03/z2ui5_cl_smps_app_032.clas.xml create mode 100644 src/13/z2ui5_cl_smps_app_031.clas.abap create mode 100644 src/13/z2ui5_cl_smps_app_031.clas.xml diff --git a/SAMPLES.md b/SAMPLES.md index 604ddd9..9a1f01f 100644 --- a/SAMPLES.md +++ b/SAMPLES.md @@ -3,7 +3,7 @@ # The sample catalogue -Every app in this repository — 48 of them — with what it shows and a +Every app in this repository — 52 of them — with what it shows and a link to its source. This is the [overview app](src/z2ui5_cl_smps_app_000.clas.abap) as a page you can read here, before installing anything. @@ -61,6 +61,9 @@ type who does not know it exists. `Ctrl+F` on this page uses them. | Update a Travel
changes single fields of one instance - UPDATE FIELDS names what may be touched
eml rap update travel modify commit table | [`Z2UI5_CL_SMPS_APP_003`](src/03/z2ui5_cl_smps_app_003.clas.abap) | | Delete a Travel
deletes one instance - MODIFY ... DELETE FROM
eml rap delete travel remove commit table | [`Z2UI5_CL_SMPS_APP_004`](src/03/z2ui5_cl_smps_app_004.clas.abap) | | Manage Travels, the Complete App
01-04 plus EXECUTE and COMMIT ENTITIES RESPONSE OF
eml rap crud travel manage popup objectstatus | [`Z2UI5_CL_SMPS_APP_005`](src/03/z2ui5_cl_smps_app_005.clas.abap) | +| Mass Change with Preview
Several travels changed in one go - the new values are previewed next to the old ones, one MODIFY ENTITIES ... UPDATE saves them, and a refusal is shown on its row
mass change bulk update preview rap eml travel multiselect | [`Z2UI5_CL_SMPS_APP_029`](src/03/z2ui5_cl_smps_app_029.clas.abap) | +| Approval Worklist
An approval worklist - the open travels, several selected and accepted or rejected with one EXECUTE, the list shrinking as they are decided
approval approve reject inbox worklist action execute rap eml travel multiselect | [`Z2UI5_CL_SMPS_APP_030`](src/03/z2ui5_cl_smps_app_030.clas.abap) | +| Edit with Conflict Check
Edit a travel without a stateful session - the app remembers LastChangedAt when it reads, and Save refuses the change if someone else saved the travel in between
edit lock conflict concurrency etag optimistic locking lastchangedat rap eml travel | [`Z2UI5_CL_SMPS_APP_032`](src/03/z2ui5_cl_smps_app_032.clas.abap) | ### RAP with Draft — `src/04` @@ -142,6 +145,7 @@ type who does not know it exists. `Ctrl+F` on this page uses them. | **Excel** — Export a Table as XLSX File with XCO
The table on the screen written into an .xlsx file with the XCO XLSX library and handed to the browser as a download - a header row, then one row per line
excel xlsx export download write spreadsheet xco table download_b64_file | [`Z2UI5_CL_SMPS_APP_022`](src/13/z2ui5_cl_smps_app_022.clas.abap) | | **E-Mail** — Send a Table as XLSX Attachment
A mail with an .xlsx attachment, sent with cl_bcs_mail_message - recipient, subject and text from the screen, the attachment written with the XCO XLSX library
email mail send attachment xlsx excel cl_bcs_mail_message smtp | [`Z2UI5_CL_SMPS_APP_023`](src/13/z2ui5_cl_smps_app_023.clas.abap) | | **Excel** — Import Travels into a RAP Business Object
An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to
excel xlsx import upload mass create rap eml business object travel fileuploader | [`Z2UI5_CL_SMPS_APP_028`](src/13/z2ui5_cl_smps_app_028.clas.abap) | +| **Excel** — Report with Selection and Export
A report as it is written every day - a selection with ranges, the result in a table with a total, and the same rows exported as .xlsx with XCO
report selection screen filter range export excel xlsx download travel sql | [`Z2UI5_CL_SMPS_APP_031`](src/13/z2ui5_cl_smps_app_031.clas.abap) | ### Adobe Forms — `src/14` diff --git a/catalogue-derived.json b/catalogue-derived.json index 42d3e0b..9e3e9c8 100644 --- a/catalogue-derived.json +++ b/catalogue-derived.json @@ -50,6 +50,9 @@ "sap.m.DatePicker", "sap.m.Dialog", "sap.m.ToolbarSpacer", + "sap.m.ObjectIdentifier", + "sap.m.SegmentedButton", + "sap.m.SegmentedButtonItem", "sap.tnt.InfoLabel", "sap.m.FeedInput", "sap.m.FeedListItem", @@ -65,14 +68,12 @@ "sap.ui.core.Item", "sap.m.ObjectNumber", "z2ui5.cc.FileUploader", - "sap.m.SegmentedButton", - "sap.m.SegmentedButtonItem", "sap.m.StepInput", "sap.m.TextArea" ], "counts": { - "samples": 48, - "controls": 61, + "samples": 52, + "controls": 62, "noChain": 0 }, "samples": [ @@ -89,22 +90,26 @@ {"class":"Z2UI5_CL_SMPS_APP_010","minUi5":"1.71","needs":[],"controls":[1,2,3,39,40,22,37,6,7,10,11,12,13,18,41,15,17,38],"controlCount":55}, {"class":"Z2UI5_CL_SMPS_APP_011","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,5,7,10,11,12,13,18,41,17,38],"controlCount":33}, {"class":"Z2UI5_CL_SMPS_APP_012","minUi5":"1.71","needs":[],"controls":[1,2,3,7,10,11,12,13,18,41,17],"controlCount":18}, - {"class":"Z2UI5_CL_SMPS_APP_013","minUi5":"1.71","needs":[],"controls":[1,52,22,37,5,23,7,53,10,41,54,17,38],"controlCount":28}, - {"class":"Z2UI5_CL_SMPS_APP_014","minUi5":"1.71","needs":[],"controls":[1,43,44,22,45,5,7,10,14,17],"controlCount":11}, - {"class":"Z2UI5_CL_SMPS_APP_015","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,5,55,7,8,10,11,12,14,17],"controlCount":38}, - {"class":"Z2UI5_CL_SMPS_APP_016","minUi5":"1.71","needs":[],"controls":[1,2,3,31,22,37,5,7,10,11,12,13,18,41,17,38,56],"controlCount":35}, - {"class":"Z2UI5_CL_SMPS_APP_017","minUi5":"1.71","needs":[],"controls":[1,2,3,31,37,5,6,7,10,51,11,12,13,18,41,17],"controlCount":31}, - {"class":"Z2UI5_CL_SMPS_APP_018","minUi5":"1.71","needs":[],"controls":[1,2,3,40,31,22,37,5,7,57,58,10,59,11,12,60,13,18,41,15,17,38],"controlCount":50}, + {"class":"Z2UI5_CL_SMPS_APP_013","minUi5":"1.71","needs":[],"controls":[1,55,22,37,5,23,7,56,10,41,57,17,38],"controlCount":28}, + {"class":"Z2UI5_CL_SMPS_APP_014","minUi5":"1.71","needs":[],"controls":[1,46,47,22,48,5,7,10,14,17],"controlCount":11}, + {"class":"Z2UI5_CL_SMPS_APP_015","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,5,58,7,8,10,11,12,14,17],"controlCount":38}, + {"class":"Z2UI5_CL_SMPS_APP_016","minUi5":"1.71","needs":[],"controls":[1,2,3,31,22,37,5,7,10,11,12,13,18,41,17,38,59],"controlCount":35}, + {"class":"Z2UI5_CL_SMPS_APP_017","minUi5":"1.71","needs":[],"controls":[1,2,3,31,37,5,6,7,10,54,11,12,13,18,41,17],"controlCount":31}, + {"class":"Z2UI5_CL_SMPS_APP_018","minUi5":"1.71","needs":[],"controls":[1,2,3,40,31,22,37,5,7,43,44,10,60,11,12,61,13,18,41,15,17,38],"controlCount":50}, {"class":"Z2UI5_CL_SMPS_APP_019","minUi5":"1.71","needs":[],"controls":[1,22,37,5,23,7,10,12,41,17,38],"controlCount":15}, {"class":"Z2UI5_CL_SMPS_APP_020","minUi5":"1.71","needs":[],"controls":[1,2,3,31,5,7,10,11,12,13,18,41,17],"controlCount":25}, - {"class":"Z2UI5_CL_SMPS_APP_021","minUi5":"1.71","needs":[],"controls":[1,2,3,31,5,7,10,11,12,17,56],"controlCount":27}, + {"class":"Z2UI5_CL_SMPS_APP_021","minUi5":"1.71","needs":[],"controls":[1,2,3,31,5,7,10,11,12,17,59],"controlCount":27}, {"class":"Z2UI5_CL_SMPS_APP_022","minUi5":"1.71","needs":[],"controls":[1,2,3,22,5,7,10,11,12,13,18,41,17],"controlCount":22}, - {"class":"Z2UI5_CL_SMPS_APP_023","minUi5":"1.71","needs":[],"controls":[1,22,37,5,23,7,10,12,60,41,17,38],"controlCount":18}, - {"class":"Z2UI5_CL_SMPS_APP_024","minUi5":"1.71","needs":[],"controls":[1,2,3,37,5,6,23,7,10,11,12,60,41,17,38],"controlCount":22}, + {"class":"Z2UI5_CL_SMPS_APP_023","minUi5":"1.71","needs":[],"controls":[1,22,37,5,23,7,10,12,61,41,17,38],"controlCount":18}, + {"class":"Z2UI5_CL_SMPS_APP_024","minUi5":"1.71","needs":[],"controls":[1,2,3,37,5,6,23,7,10,11,12,61,41,17,38],"controlCount":22}, {"class":"Z2UI5_CL_SMPS_APP_025","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,5,23,7,10,11,12,13,18,41,17,38],"controlCount":31}, - {"class":"Z2UI5_CL_SMPS_APP_026","minUi5":"1.71","needs":[],"controls":[2,3,22,37,4,45,5,7,9,10,50,11,12,13,18,15,17,38],"controlCount":24}, - {"class":"Z2UI5_CL_SMPS_APP_027","minUi5":"1.71","needs":[],"controls":[2,3,37,5,7,10,51,11,12,18,17],"controlCount":12}, - {"class":"Z2UI5_CL_SMPS_APP_028","minUi5":"1.71","needs":[],"controls":[1,52,2,3,31,5,6,7,10,11,12,13,18,41,17,56],"controlCount":38}, + {"class":"Z2UI5_CL_SMPS_APP_026","minUi5":"1.71","needs":[],"controls":[2,3,22,37,4,48,5,7,9,10,53,11,12,13,18,15,17,38],"controlCount":24}, + {"class":"Z2UI5_CL_SMPS_APP_027","minUi5":"1.71","needs":[],"controls":[2,3,37,5,7,10,54,11,12,18,17],"controlCount":12}, + {"class":"Z2UI5_CL_SMPS_APP_028","minUi5":"1.71","needs":[],"controls":[1,55,2,3,31,5,6,7,10,11,12,13,18,41,17,59],"controlCount":38}, + {"class":"Z2UI5_CL_SMPS_APP_029","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,42,6,7,10,11,12,13,18,41,17,38],"controlCount":34}, + {"class":"Z2UI5_CL_SMPS_APP_030","minUi5":"1.71","needs":[],"controls":[1,2,3,42,6,7,43,44,10,11,12,18,41,17],"controlCount":29}, + {"class":"Z2UI5_CL_SMPS_APP_031","minUi5":"1.71","needs":[],"controls":[1,2,3,39,31,22,37,7,10,60,11,12,13,18,41,17,38],"controlCount":44}, + {"class":"Z2UI5_CL_SMPS_APP_032","minUi5":"1.71","needs":[],"controls":[1,2,3,40,22,37,5,7,10,11,12,13,18,41,15,17,38],"controlCount":38}, {"class":"Z2UI5_CL_SMPS_APP_313","minUi5":"1.71","needs":[],"controls":[7,10,19,20,21,17],"controlCount":6}, {"class":"Z2UI5_CL_SMPS_APP_314","minUi5":"1.71","needs":[],"controls":[2,3,22,7,10,11,12,13,18,17],"controlCount":31}, {"class":"Z2UI5_CL_SMPS_APP_315","minUi5":"1.71","needs":[],"controls":[2,3,7,10,11,12,13,18,17],"controlCount":35}, @@ -118,10 +123,10 @@ {"class":"Z2UI5_CL_SMPS_APP_482","minUi5":"1.71","needs":[],"controls":[1,22,37,5,7,10,17,38],"controlCount":10}, {"class":"Z2UI5_CL_SMPS_APP_483","minUi5":"1.71","needs":[],"controls":[1,22,37,5,7,10,17,38],"controlCount":11}, {"class":"Z2UI5_CL_SMPS_APP_484","minUi5":"1.71","needs":[],"controls":[1,22,37,5,7,10,17,38],"controlCount":13}, - {"class":"Z2UI5_CL_SMPS_APP_485","minUi5":"1.71","needs":[],"controls":[1,31,37,5,7,10,14,42,17],"controlCount":18}, - {"class":"Z2UI5_CL_SMPS_APP_486","minUi5":"1.71","needs":[],"controls":[1,31,37,7,10,14,42,17],"controlCount":11}, + {"class":"Z2UI5_CL_SMPS_APP_485","minUi5":"1.71","needs":[],"controls":[1,31,37,5,7,10,14,45,17],"controlCount":18}, + {"class":"Z2UI5_CL_SMPS_APP_486","minUi5":"1.71","needs":[],"controls":[1,31,37,7,10,14,45,17],"controlCount":11}, {"class":"Z2UI5_CL_SMPS_APP_487","minUi5":"1.71","needs":[],"controls":[1,22,5,7,10,12,14,17],"controlCount":9}, - {"class":"Z2UI5_CL_SMPS_APP_489","minUi5":"1.71","needs":[],"controls":[1,43,44,22,37,45,5,23,7,46,47,10,48,41,42,15,17,38,49],"controlCount":21}, + {"class":"Z2UI5_CL_SMPS_APP_489","minUi5":"1.71","needs":[],"controls":[1,46,47,22,37,48,5,23,7,49,50,10,51,41,45,15,17,38,52],"controlCount":21}, {"class":"Z2UI5_CL_SMPS_APP_490","minUi5":"1.71","needs":[],"controls":[1,31,22,5,7,10,13,14,17,38],"controlCount":14}, {"class":"Z2UI5_CL_SMPS_APP_493","minUi5":"1.71","needs":[],"controls":[2,3,31,22,5,7,10,11,12,35,36,32,17],"controlCount":27} ] diff --git a/catalogue.json b/catalogue.json index 7f55941..dbb9671 100644 --- a/catalogue.json +++ b/catalogue.json @@ -536,6 +536,82 @@ "branch": "03-rap", "setup": "src/03/README.md" }, + { + "class": "Z2UI5_CL_SMPS_APP_029", + "path": "src/03/z2ui5_cl_smps_app_029.clas.abap", + "package": "src/03", + "technology": "RAP", + "title": "Mass Change with Preview", + "summary": "Several travels changed in one go - the new values are previewed next to the old ones, one MODIFY ENTITIES ... UPDATE saves them, and a refusal is shown on its row", + "keywords": [ + "mass", + "change", + "bulk", + "update", + "preview", + "rap", + "eml", + "travel", + "multiselect" + ], + "runsOn": "Cloud + Standard ≥ 7.54 (1909)", + "cloud": true, + "needs": "ABAP Platform >= 1909; the BO ships with this repo", + "branch": "03-rap", + "setup": "src/03/README.md" + }, + { + "class": "Z2UI5_CL_SMPS_APP_030", + "path": "src/03/z2ui5_cl_smps_app_030.clas.abap", + "package": "src/03", + "technology": "RAP", + "title": "Approval Worklist", + "summary": "An approval worklist - the open travels, several selected and accepted or rejected with one EXECUTE, the list shrinking as they are decided", + "keywords": [ + "approval", + "approve", + "reject", + "inbox", + "worklist", + "action", + "execute", + "rap", + "eml", + "travel", + "multiselect" + ], + "runsOn": "Cloud + Standard ≥ 7.54 (1909)", + "cloud": true, + "needs": "ABAP Platform >= 1909; the BO ships with this repo", + "branch": "03-rap", + "setup": "src/03/README.md" + }, + { + "class": "Z2UI5_CL_SMPS_APP_032", + "path": "src/03/z2ui5_cl_smps_app_032.clas.abap", + "package": "src/03", + "technology": "RAP", + "title": "Edit with Conflict Check", + "summary": "Edit a travel without a stateful session - the app remembers LastChangedAt when it reads, and Save refuses the change if someone else saved the travel in between", + "keywords": [ + "edit", + "lock", + "conflict", + "concurrency", + "etag", + "optimistic", + "locking", + "lastchangedat", + "rap", + "eml", + "travel" + ], + "runsOn": "Cloud + Standard ≥ 7.54 (1909)", + "cloud": true, + "needs": "ABAP Platform >= 1909; the BO ships with this repo", + "branch": "03-rap", + "setup": "src/03/README.md" + }, { "class": "Z2UI5_CL_SMPS_APP_006", "path": "src/04/z2ui5_cl_smps_app_006.clas.abap", @@ -1261,6 +1337,32 @@ "branch": "13-excel-email", "setup": "src/13/README.md" }, + { + "class": "Z2UI5_CL_SMPS_APP_031", + "path": "src/13/z2ui5_cl_smps_app_031.clas.abap", + "package": "src/13", + "technology": "Excel & E-Mail", + "title": "Excel — Report with Selection and Export", + "summary": "A report as it is written every day - a selection with ranges, the result in a table with a total, and the same rows exported as .xlsx with XCO", + "keywords": [ + "report", + "selection", + "screen", + "filter", + "range", + "export", + "excel", + "xlsx", + "download", + "travel", + "sql" + ], + "runsOn": "Cloud + Standard ≥ 7.58 (2023)", + "cloud": true, + "needs": "the XCO XLSX library; outbound mail for the mail sample", + "branch": "13-excel-email", + "setup": "src/13/README.md" + }, { "class": "Z2UI5_CL_SMPS_APP_024", "path": "src/14/z2ui5_cl_smps_app_024.clas.abap", diff --git a/src/03/README.md b/src/03/README.md index 41a14a3..6b3bc60 100644 --- a/src/03/README.md +++ b/src/03/README.md @@ -85,8 +85,8 @@ rest of the samples read themselves: ## Find the snippet -Five samples: four single statements, then one complete app. The numbers are the -reading order. +Four single statements, then one complete app, then three everyday jobs. The +numbers are the reading order. | You want to | Statement | Sample | |---|---|---| @@ -105,6 +105,16 @@ made sense: |---|---|---| | call a BO action, save and catch what failed | `MODIFY … EXECUTE`, `COMMIT ENTITIES RESPONSE OF` | [`005` manage travels](z2ui5_cl_smps_app_005.clas.abap) | +**Everyday jobs** — three apps that each do one thing a real project needs, on the +same business object. Each one is the worked example of a guide in the +[documentation](https://abap2ui5.github.io/docs/everyday/): + +| | | | +|---|---|---| +| change many travels at once, preview first, every refusal on its row | `MODIFY … UPDATE` for all rows, answers by key | [`029` mass change](z2ui5_cl_smps_app_029.clas.abap) | +| a worklist of open travels, accept or reject several at once | `MODIFY … EXECUTE … RESULT` | [`030` approval worklist](z2ui5_cl_smps_app_030.clas.abap) | +| edit without a lock while typing, refuse a save over someone else's | `LastChangedAt` checked after the `UPDATE` took the lock | [`032` edit with conflict check](z2ui5_cl_smps_app_032.clas.abap) | + ## The snippets **Read** — no SELECT, no OData. diff --git a/src/03/z2ui5_cl_smps_app_029.clas.abap b/src/03/z2ui5_cl_smps_app_029.clas.abap new file mode 100644 index 0000000..00402b0 --- /dev/null +++ b/src/03/z2ui5_cl_smps_app_029.clas.abap @@ -0,0 +1,394 @@ +" @keywords mass change bulk update preview rap eml travel multiselect +" @summary Several travels changed in one go - the new values are previewed next to the old ones, one MODIFY ENTITIES ... UPDATE saves them, and a refusal is shown on its row +"!

RAP - Mass Change with Preview

+"! Select travels, say what should change, look at the result, then save: +"! +"! 1. the change is described once - a new booking fee, a number of days to +"! move the begin date and the end date by +"! 2. Preview computes the new values for every selected travel and shows +"! them next to the current ones. Nothing is sent to the business object +"! 3. Apply sends all of them in one statement and saves: +"! +"! MODIFY ENTITIES OF z2ui5_r_smps_trv +"! ENTITY travel +"! UPDATE FIELDS ( bookingfee begindate enddate ) +"! WITH VALUE #( FOR p IN t_preview ( travelid = p-travel_id ... ) ) +"! FAILED DATA(s_failed) +"! REPORTED DATA(s_reported). +"! +"! COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv +"! FAILED DATA(s_failed_commit) +"! REPORTED DATA(s_reported_commit). +"! +"! An UPDATE names its instances by key, so every answer of the business +"! object - FAILED and REPORTED, from the UPDATE and from the validations at +"! COMMIT - carries the key of the row it is about. Moving only the begin +"! date past the end date is refused by validateDates, and that refusal is +"! shown on its row. The COMMIT is all or nothing. +CLASS z2ui5_cl_smps_app_029 DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_travel, + selkz TYPE abap_bool, + travel_id TYPE string, + customer_id TYPE string, + begin_date TYPE string, + end_date TYPE string, + booking_fee TYPE string, + currency TYPE string, + new_begin_date TYPE string, + new_end_date TYPE string, + new_fee TYPE string, + "! None, Success, Warning or Error - the sap.ui.core.ValueState + state TYPE string, + status TYPE string, + END OF ty_s_travel. + TYPES ty_t_travel TYPE STANDARD TABLE OF ty_s_travel WITH EMPTY KEY. + DATA t_travels TYPE ty_t_travel. + + TYPES: + BEGIN OF ty_s_change, + "! empty keeps the fee as it is + booking_fee TYPE string, + begin_shift TYPE string, + end_shift TYPE string, + END OF ty_s_change. + DATA s_change TYPE ty_s_change. + + "! true once Preview computed the new values - Apply saves exactly those + DATA check_preview TYPE abap_bool. + + PROTECTED SECTION. + + TYPES: + BEGIN OF ty_s_update, + travel_id TYPE z2ui5_t_smps_trv-travel_id, + begin_date TYPE d, + end_date TYPE d, + booking_fee TYPE z2ui5_t_smps_trv-booking_fee, + END OF ty_s_update. + TYPES ty_t_update TYPE STANDARD TABLE OF ty_s_update WITH EMPTY KEY. + + DATA client TYPE REF TO z2ui5_if_client. + + METHODS on_event. + METHODS data_read. + METHODS preview_build. + METHODS changes_apply. + METHODS view_display. + + "! the new values of the previewed rows, in the types of the business object + METHODS updates_get + RETURNING + VALUE(result) TYPE ty_t_update. + + METHODS row_refuse + IMPORTING + travel_id TYPE clike + text TYPE string. + + PRIVATE SECTION. +ENDCLASS. + + +CLASS z2ui5_cl_smps_app_029 IMPLEMENTATION. + + METHOD z2ui5_if_app~main. + + me->client = client. + IF client->check_on_init( ). + s_change = VALUE #( begin_shift = `0` end_shift = `0` ). + data_read( ). + view_display( ). + ELSEIF client->check_on_navigated( ). + view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). + ENDIF. + + ENDMETHOD. + + + METHOD on_event. + + CASE client->get_event( ). + WHEN `PREVIEW`. + preview_build( ). + WHEN `APPLY`. + changes_apply( ). + WHEN `REFRESH`. + data_read( ). + WHEN `GENERATE`. + client->message_toast_display( z2ui5_cl_smps_data_trv=>data_reset( ) ). + data_read( ). + ENDCASE. + view_display( ). + + ENDMETHOD. + + + METHOD data_read. + + SELECT FROM z2ui5_r_smps_trv "#EC CI_NOWHERE + FIELDS TravelId, CustomerId, BeginDate, EndDate, BookingFee, CurrencyCode + ORDER BY TravelId DESCENDING + INTO TABLE @DATA(t_result) UP TO 50 ROWS. + + t_travels = VALUE #( FOR r IN t_result + ( travel_id = |{ r-travelid ALPHA = OUT }| + customer_id = |{ r-customerid ALPHA = OUT }| + begin_date = |{ r-begindate DATE = ISO }| + end_date = |{ r-enddate DATE = ISO }| + booking_fee = |{ r-bookingfee }| + currency = |{ r-currencycode }| + state = `None` ) ). + check_preview = abap_false. + + ENDMETHOD. + + + METHOD preview_build. + + " the change is checked once, before it is applied to any row - a fee + " that is no number is one message, not one per row + DATA fee TYPE z2ui5_t_smps_trv-booking_fee. + DATA begin_shift TYPE i. + DATA end_shift TYPE i. + TRY. + IF s_change-booking_fee IS NOT INITIAL. + fee = s_change-booking_fee. + ENDIF. + begin_shift = s_change-begin_shift. + end_shift = s_change-end_shift. + CATCH cx_sy_conversion_error. + client->message_box_display( text = `The fee must be a number like 20.00, the shifts whole days like 7 or -3` + type = `error` ). + RETURN. + ENDTRY. + + IF NOT line_exists( t_travels[ selkz = abap_true ] ). + client->message_box_display( `Select the travels to change first` ). + RETURN. + ENDIF. + + LOOP AT t_travels REFERENCE INTO DATA(travel). + CLEAR: travel->new_begin_date, travel->new_end_date, travel->new_fee. + travel->state = `None`. + travel->status = ``. + IF travel->selkz = abap_false. + CONTINUE. + ENDIF. + + " the dates are shown as ISO text - back to a date, moved, and shown again + DATA(begin_date) = CONV d( replace( val = travel->begin_date sub = `-` with = `` occ = 0 ) ). + DATA(end_date) = CONV d( replace( val = travel->end_date sub = `-` with = `` occ = 0 ) ). + travel->new_begin_date = |{ CONV d( begin_date + begin_shift ) DATE = ISO }|. + travel->new_end_date = |{ CONV d( end_date + end_shift ) DATE = ISO }|. + travel->new_fee = COND #( WHEN s_change-booking_fee IS INITIAL THEN travel->booking_fee ELSE |{ fee }| ). + travel->status = `Will change`. + ENDLOOP. + + check_preview = abap_true. + + ENDMETHOD. + + + METHOD updates_get. + + LOOP AT t_travels INTO DATA(travel) WHERE new_begin_date IS NOT INITIAL. + APPEND VALUE #( travel_id = travel-travel_id + begin_date = replace( val = travel-new_begin_date sub = `-` with = `` occ = 0 ) + end_date = replace( val = travel-new_end_date sub = `-` with = `` occ = 0 ) + booking_fee = travel-new_fee ) TO result. + ENDLOOP. + + ENDMETHOD. + + + METHOD changes_apply. + + IF check_preview = abap_false. + client->message_box_display( `Press Preview first - Apply saves what the preview shows` ). + RETURN. + ENDIF. + + DATA(t_update) = updates_get( ). + + " one statement for every row - the key names the instance, and every + " answer of the business object carries that key back + MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + UPDATE FIELDS ( bookingfee begindate enddate ) + WITH VALUE #( FOR u IN t_update + ( travelid = u-travel_id + bookingfee = u-booking_fee + begindate = u-begin_date + enddate = u-end_date ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + + IF s_failed-travel IS NOT INITIAL. + ROLLBACK ENTITIES. + LOOP AT s_failed-travel INTO DATA(failed). + row_refuse( travel_id = |{ failed-travelid ALPHA = OUT }| + text = concat_lines_of( sep = `; ` + table = VALUE string_table( FOR r IN s_reported-travel + WHERE ( travelid = failed-travelid AND %msg IS BOUND ) + ( r-%msg->if_message~get_text( ) ) ) ) ). + ENDLOOP. + client->message_box_display( text = `The business object refused some rows - nothing was saved` type = `error` ). + RETURN. + ENDIF. + + " the validations run now: validateDates refuses an end date before the + " begin date, which a shift of only one of them can produce + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). + + IF s_failed_commit IS NOT INITIAL. + " a failed save keeps the changes in the transactional buffer - discard + " them, or abap2UI5's own COMMIT WORK at the end of the roundtrip runs + " into them + ROLLBACK ENTITIES. + LOOP AT s_failed_commit-travel INTO DATA(failed_commit). + row_refuse( travel_id = |{ failed_commit-travelid ALPHA = OUT }| + text = concat_lines_of( sep = `; ` + table = VALUE string_table( FOR r IN s_reported_commit-travel + WHERE ( travelid = failed_commit-travelid AND %msg IS BOUND ) + ( r-%msg->if_message~get_text( ) ) ) ) ). + ENDLOOP. + client->message_box_display( text = `The business object refused some rows - nothing was saved` type = `error` ). + RETURN. + ENDIF. + + client->message_toast_display( |{ lines( t_update ) } travels changed| ). + data_read( ). + + ENDMETHOD. + + + METHOD row_refuse. + + LOOP AT t_travels REFERENCE INTO DATA(travel) WHERE new_begin_date IS NOT INITIAL. + IF travel->travel_id = travel_id. + travel->state = `Error`. + " a business object may refuse without a message - saying so beats + " an empty cell + travel->status = COND #( WHEN text IS INITIAL THEN `Refused by the business object` ELSE text ). + ELSEIF travel->state <> `Error`. + travel->state = `Warning`. + travel->status = `Not saved - another row was refused`. + ENDIF. + ENDLOOP. + + ENDMETHOD. + + + METHOD view_display. + + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `View` ns = `mvc` + )->a( n = `displayBlock` v = `true` + )->a( n = `height` v = `100%` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` + )->a( n = `xmlns:form` v = `sap.ui.layout.form` ). + DATA(page) = view->ele( `Shell` + )->ele( `Page` + )->a( n = `title` v = `abap2UI5 - EML - 29 Mass Change with Preview` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + + page->ele( n = `SimpleForm` ns = `form` + )->a( n = `title` v = `Change the selected travels` + )->a( n = `editable` b = abap_true + )->ele( n = `content` ns = `form` + )->tag( `Label` + )->a( n = `text` v = `New booking fee` + )->tag( `Input` + )->a( n = `placeholder` v = `empty keeps the fee` + )->a( n = `value` v = client->_bind( s_change-booking_fee ) + )->tag( `Label` + )->a( n = `text` v = `Move begin date by days` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_change-begin_shift ) + )->tag( `Label` + )->a( n = `text` v = `Move end date by days` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_change-end_shift ) ). + + DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `mode` v = `MultiSelect` + )->a( n = `noDataText` v = `No travels yet - press Reset Demo Data` ). + table->ele( `headerToolbar` + )->ele( `Toolbar` + )->tag( `Title` + )->a( n = `text` v = `Travels (Z2UI5_R_SMPS_TRV)` + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `PREVIEW` ) + )->a( n = `text` v = `Preview` + )->tag( `Button` + )->a( n = `press` v = client->_event( `APPLY` ) + )->a( n = `text` v = `Apply` + )->a( n = `type` v = `Emphasized` + )->a( n = `enabled` b = check_preview + )->tag( `Button` + )->a( n = `press` v = client->_event( `GENERATE` ) + )->a( n = `text` v = `Reset Demo Data` + )->a( n = `tooltip` v = `Deletes every travel, then creates the demo travels again` + )->tag( `Button` + )->a( n = `press` v = client->_event( `REFRESH` ) + )->a( n = `icon` v = `sap-icon://refresh` + )->a( n = `tooltip` v = `Refresh` ). + + table->ele( `columns` + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Travel` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Begin Date` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `End Date` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Booking Fee` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Status` ). + + table->ele( `items` + )->ele( `ColumnListItem` + )->a( n = `selected` v = `{SELKZ}` + )->ele( `cells` + )->tag( `Text` + )->a( n = `text` v = `{TRAVEL_ID}` + )->tag( `ObjectIdentifier` + )->a( n = `title` v = `{NEW_BEGIN_DATE}` + )->a( n = `text` v = `{BEGIN_DATE}` + )->tag( `ObjectIdentifier` + )->a( n = `title` v = `{NEW_END_DATE}` + )->a( n = `text` v = `{END_DATE}` + )->tag( `ObjectIdentifier` + )->a( n = `title` v = `{NEW_FEE}` + )->a( n = `text` v = `{BOOKING_FEE} {CURRENCY}` + )->tag( `ObjectStatus` + )->a( n = `text` v = `{STATUS}` + )->a( n = `state` v = `{STATE}` ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/03/z2ui5_cl_smps_app_029.clas.xml b/src/03/z2ui5_cl_smps_app_029.clas.xml new file mode 100644 index 0000000..a889b70 --- /dev/null +++ b/src/03/z2ui5_cl_smps_app_029.clas.xml @@ -0,0 +1,16 @@ + + + + + + Z2UI5_CL_SMPS_APP_029 + E + RAP - Mass Change with Preview + 1 + X + X + X + + + + diff --git a/src/03/z2ui5_cl_smps_app_030.clas.abap b/src/03/z2ui5_cl_smps_app_030.clas.abap new file mode 100644 index 0000000..71935fd --- /dev/null +++ b/src/03/z2ui5_cl_smps_app_030.clas.abap @@ -0,0 +1,277 @@ +" @keywords approval approve reject inbox worklist action execute rap eml travel multiselect +" @summary An approval worklist - the open travels, several selected and accepted or rejected with one EXECUTE, the list shrinking as they are decided +"!

RAP - Approval Worklist

+"! A worklist, not a maintenance screen: it shows what is waiting for a +"! decision - the open travels - and gets shorter with every decision. The +"! user selects one or many and presses Accept or Reject; the app calls the +"! action of the business object for all of them in one statement: +"! +"! MODIFY ENTITIES OF z2ui5_r_smps_trv +"! ENTITY travel +"! EXECUTE acceptTravel FROM VALUE #( FOR k IN t_keys ( travelid = k ) ) +"! RESULT DATA(t_result) +"! FAILED DATA(s_failed) +"! REPORTED DATA(s_reported). +"! +"! The action changes the status - the app never writes OverallStatus +"! itself, which the behavior definition declares readonly. RESULT hands +"! back the changed instances, so the toast can say what happened without +"! reading them again. The filter switches between the worklist and the +"! decided travels, so a decision can be looked up afterwards. +CLASS z2ui5_cl_smps_app_030 DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_travel, + selkz TYPE abap_bool, + travel_id TYPE string, + customer_id TYPE string, + begin_date TYPE string, + end_date TYPE string, + total_price TYPE string, + description TYPE string, + status_text TYPE string, + status_state TYPE string, + END OF ty_s_travel. + TYPES ty_t_travel TYPE STANDARD TABLE OF ty_s_travel WITH EMPTY KEY. + DATA t_travels TYPE ty_t_travel. + + "! OPEN - the worklist, DONE - the decided travels + DATA filter TYPE string. + + PROTECTED SECTION. + DATA client TYPE REF TO z2ui5_if_client. + + METHODS on_event. + METHODS data_read. + METHODS view_display. + + "! accepts or rejects every selected travel with one EXECUTE + METHODS travels_decide + IMPORTING + accept TYPE abap_bool. + + PRIVATE SECTION. +ENDCLASS. + + +CLASS z2ui5_cl_smps_app_030 IMPLEMENTATION. + + METHOD z2ui5_if_app~main. + + me->client = client. + IF client->check_on_init( ). + filter = `OPEN`. + data_read( ). + view_display( ). + ELSEIF client->check_on_navigated( ). + view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). + ENDIF. + + ENDMETHOD. + + + METHOD on_event. + + CASE client->get_event( ). + WHEN `ACCEPT`. + travels_decide( abap_true ). + WHEN `REJECT`. + travels_decide( abap_false ). + WHEN `FILTER` OR `REFRESH`. + data_read( ). + WHEN `GENERATE`. + client->message_toast_display( z2ui5_cl_smps_data_trv=>data_reset( ) ). + data_read( ). + ENDCASE. + view_display( ). + + ENDMETHOD. + + + METHOD data_read. + + " the worklist is what still waits for a decision - the filter is part + " of the SELECT, so a decided travel leaves the list on the next read + TYPES ty_r_status TYPE RANGE OF z2ui5_t_smps_trv-overall_status. + DATA(t_status) = VALUE ty_r_status( ( sign = `I` + option = COND #( WHEN filter = `OPEN` THEN `EQ` ELSE `NE` ) + low = z2ui5_cl_smps_context=>cs_status-open ) ). + + SELECT FROM z2ui5_r_smps_trv + FIELDS TravelId, CustomerId, BeginDate, EndDate, TotalPrice, CurrencyCode, Description, OverallStatus + WHERE OverallStatus IN @t_status + ORDER BY BeginDate, TravelId + INTO TABLE @DATA(t_result) UP TO 100 ROWS. + + t_travels = VALUE #( FOR r IN t_result + ( travel_id = |{ r-travelid ALPHA = OUT }| + customer_id = |{ r-customerid ALPHA = OUT }| + begin_date = |{ r-begindate DATE = ISO }| + end_date = |{ r-enddate DATE = ISO }| + total_price = |{ r-totalprice } { r-currencycode }| + description = |{ r-description }| + status_text = z2ui5_cl_smps_context=>status_get_text( r-overallstatus ) + status_state = z2ui5_cl_smps_context=>status_get_state( r-overallstatus ) ) ). + + ENDMETHOD. + + + METHOD travels_decide. + + DATA(t_keys) = VALUE string_table( FOR t IN t_travels WHERE ( selkz = abap_true ) ( t-travel_id ) ). + IF t_keys IS INITIAL. + client->message_box_display( `Select the travels to decide first` ). + RETURN. + ENDIF. + + " an action is called like any other operation - by key, many at once. + " Two statements rather than a dynamic action name: the action is part + " of the statement, which is what lets the compiler check it + IF accept = abap_true. + MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + EXECUTE acceptTravel FROM VALUE #( FOR k IN t_keys ( travelid = k ) ) + RESULT DATA(t_result) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + ELSE. + MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + EXECUTE rejectTravel FROM VALUE #( FOR k IN t_keys ( travelid = k ) ) + RESULT DATA(t_result_reject) + FAILED s_failed + REPORTED s_reported. + t_result = CORRESPONDING #( t_result_reject ). + ENDIF. + + IF s_failed-travel IS NOT INITIAL. + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). + RETURN. + ENDIF. + + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). + + IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). + RETURN. + ENDIF. + + " RESULT carries the changed instances - the new status comes from the + " business object, not from what the app expected it to be + DATA(ids) = concat_lines_of( sep = `, ` + table = VALUE string_table( FOR r IN t_result ( |{ r-travelid ALPHA = OUT }| ) ) ). + DATA(status) = z2ui5_cl_smps_context=>status_get_text( VALUE #( t_result[ 1 ]-%param-overallstatus OPTIONAL ) ). + client->message_toast_display( |{ status }: { ids }| ). + data_read( ). + + ENDMETHOD. + + + METHOD view_display. + + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `View` ns = `mvc` + )->a( n = `displayBlock` v = `true` + )->a( n = `height` v = `100%` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ). + DATA(page) = view->ele( `Shell` + )->ele( `Page` + )->a( n = `title` v = `abap2UI5 - EML - 30 Approval Worklist` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + + DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `mode` v = COND #( WHEN filter = `OPEN` THEN `MultiSelect` ELSE `None` ) + )->a( n = `noDataText` v = COND #( WHEN filter = `OPEN` + THEN `Nothing waits for a decision - press Reset Demo Data for open travels` + ELSE `No travel decided yet` ) ). + table->ele( `headerToolbar` + )->ele( `Toolbar` + )->ele( `SegmentedButton` + )->a( n = `selectedKey` v = client->_bind( filter ) + )->a( n = `selectionChange` v = client->_event( `FILTER` ) + )->ele( `items` + )->tag( `SegmentedButtonItem` + )->a( n = `key` v = `OPEN` + )->a( n = `text` v = `To Decide` + )->tag( `SegmentedButtonItem` + )->a( n = `key` v = `DONE` + )->a( n = `text` v = `Decided` + )->end( + )->end( + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `ACCEPT` ) + )->a( n = `text` v = `Accept` + )->a( n = `icon` v = `sap-icon://accept` + )->a( n = `type` v = `Accept` + )->a( n = `visible` b = xsdbool( filter = `OPEN` ) + )->tag( `Button` + )->a( n = `press` v = client->_event( `REJECT` ) + )->a( n = `text` v = `Reject` + )->a( n = `icon` v = `sap-icon://decline` + )->a( n = `type` v = `Reject` + )->a( n = `visible` b = xsdbool( filter = `OPEN` ) + )->tag( `Button` + )->a( n = `press` v = client->_event( `GENERATE` ) + )->a( n = `text` v = `Reset Demo Data` + )->a( n = `tooltip` v = `Deletes every travel, then creates the demo travels again` + )->tag( `Button` + )->a( n = `press` v = client->_event( `REFRESH` ) + )->a( n = `icon` v = `sap-icon://refresh` + )->a( n = `tooltip` v = `Refresh` ). + + table->ele( `columns` + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Travel` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Customer` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Dates` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Total Price` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Status` ). + + table->ele( `items` + )->ele( `ColumnListItem` + )->a( n = `selected` v = `{SELKZ}` + )->ele( `cells` + )->tag( `ObjectIdentifier` + )->a( n = `title` v = `{TRAVEL_ID}` + )->a( n = `text` v = `{DESCRIPTION}` + )->tag( `Text` + )->a( n = `text` v = `{CUSTOMER_ID}` + )->tag( `Text` + )->a( n = `text` v = `{BEGIN_DATE} - {END_DATE}` + )->tag( `Text` + )->a( n = `text` v = `{TOTAL_PRICE}` + )->tag( `ObjectStatus` + )->a( n = `text` v = `{STATUS_TEXT}` + )->a( n = `state` v = `{STATUS_STATE}` ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/03/z2ui5_cl_smps_app_030.clas.xml b/src/03/z2ui5_cl_smps_app_030.clas.xml new file mode 100644 index 0000000..80d9c7b --- /dev/null +++ b/src/03/z2ui5_cl_smps_app_030.clas.xml @@ -0,0 +1,16 @@ + + + + + + Z2UI5_CL_SMPS_APP_030 + E + RAP - Approval Worklist + 1 + X + X + X + + + + diff --git a/src/03/z2ui5_cl_smps_app_032.clas.abap b/src/03/z2ui5_cl_smps_app_032.clas.abap new file mode 100644 index 0000000..ba3e556 --- /dev/null +++ b/src/03/z2ui5_cl_smps_app_032.clas.abap @@ -0,0 +1,390 @@ +" @keywords edit lock conflict concurrency etag optimistic locking lastchangedat rap eml travel +" @summary Edit a travel without a stateful session - the app remembers LastChangedAt when it reads, and Save refuses the change if someone else saved the travel in between +"!

RAP - Edit with Conflict Check

+"! Optimistic locking in a stateless app. Nothing is locked while the user +"! types - an abap2UI5 app holds no session between two roundtrips, so a +"! lock set on Edit would be gone before Save. Instead the app remembers the +"! ETag of the business object, the field LastChangedAt, when it reads the +"! travel, and Save checks it: +"! +"! MODIFY ENTITIES OF z2ui5_r_smps_trv +"! ENTITY travel +"! UPDATE FIELDS ( description bookingfee ) WITH VALUE #( ( ... ) ) +"! FAILED DATA(s_failed) +"! REPORTED DATA(s_reported). +"! +"! SELECT SINGLE FROM z2ui5_r_smps_trv FIELDS LastChangedAt +"! WHERE TravelId = @travel_id INTO @DATA(current). +"! +"! IF current <> s_edit-last_changed_at. " someone saved in between +"! ROLLBACK ENTITIES. +"! +"! The order matters. The UPDATE comes first because it takes the lock of +"! the business object (lock master), and the check after it reads the +"! database, where the change of this roundtrip is not written yet - so +"! between the check and the COMMIT nobody else can save the travel. +"! "Change by Someone Else" saves the travel in a roundtrip of its own, which +"! is what a second user would do. +CLASS z2ui5_cl_smps_app_032 DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_travel, + travel_id TYPE string, + customer_id TYPE string, + booking_fee TYPE string, + currency TYPE string, + description TYPE string, + changed_at TYPE string, + END OF ty_s_travel. + TYPES ty_t_travel TYPE STANDARD TABLE OF ty_s_travel WITH EMPTY KEY. + DATA t_travels TYPE ty_t_travel. + + TYPES: + BEGIN OF ty_s_edit, + travel_id TYPE string, + booking_fee TYPE string, + currency TYPE string, + description TYPE string, + "! the ETag as it was when the travel was read - what Save compares + last_changed_at TYPE timestampl, + END OF ty_s_edit. + DATA s_edit TYPE ty_s_edit. + + PROTECTED SECTION. + DATA client TYPE REF TO z2ui5_if_client. + + METHODS on_event. + METHODS data_read. + METHODS travel_edit. + METHODS travel_save. + METHODS travel_change_elsewhere. + METHODS view_display. + METHODS popup_edit_display. + + PRIVATE SECTION. +ENDCLASS. + + +CLASS z2ui5_cl_smps_app_032 IMPLEMENTATION. + + METHOD z2ui5_if_app~main. + + me->client = client. + IF client->check_on_init( ). + data_read( ). + view_display( ). + ELSEIF client->check_on_navigated( ). + view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). + ENDIF. + + ENDMETHOD. + + + METHOD on_event. + + CASE client->get_event( ). + WHEN `EDIT`. + travel_edit( ). + WHEN `SAVE`. + travel_save( ). + WHEN `ELSEWHERE`. + travel_change_elsewhere( ). + WHEN `CANCEL`. + client->popup_destroy( ). + WHEN `REFRESH`. + data_read( ). + view_display( ). + WHEN `GENERATE`. + client->message_toast_display( z2ui5_cl_smps_data_trv=>data_reset( ) ). + data_read( ). + view_display( ). + ENDCASE. + + ENDMETHOD. + + + METHOD data_read. + + SELECT FROM z2ui5_r_smps_trv "#EC CI_NOWHERE + FIELDS TravelId, CustomerId, BookingFee, CurrencyCode, Description, LastChangedAt + ORDER BY TravelId DESCENDING + INTO TABLE @DATA(t_result) UP TO 20 ROWS. + + t_travels = VALUE #( FOR r IN t_result + ( travel_id = |{ r-travelid ALPHA = OUT }| + customer_id = |{ r-customerid ALPHA = OUT }| + booking_fee = |{ r-bookingfee }| + currency = |{ r-currencycode }| + description = |{ r-description }| + changed_at = |{ r-lastchangedat TIMESTAMP = ISO }| ) ). + + ENDMETHOD. + + + METHOD travel_edit. + + DATA travel_id TYPE z2ui5_t_smps_trv-travel_id. + travel_id = client->get_event_arg( ). + + " read fresh from the business object, not from the list - the ETag + " remembered here is the one the user's change is based on + READ ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + FIELDS ( bookingfee currencycode description lastchangedat ) + WITH VALUE #( ( travelid = travel_id ) ) + RESULT DATA(t_result) + FAILED DATA(s_failed). + + IF s_failed-travel IS NOT INITIAL OR t_result IS INITIAL. + client->message_box_display( text = `The travel does not exist any more - press Refresh` type = `error` ). + RETURN. + ENDIF. + + DATA(s_result) = t_result[ 1 ]. + s_edit = VALUE #( travel_id = |{ travel_id ALPHA = OUT }| + booking_fee = |{ s_result-bookingfee }| + currency = |{ s_result-currencycode }| + description = |{ s_result-description }| + last_changed_at = s_result-lastchangedat ). + popup_edit_display( ). + + ENDMETHOD. + + + METHOD travel_save. + + DATA travel_id TYPE z2ui5_t_smps_trv-travel_id. + DATA booking_fee TYPE z2ui5_t_smps_trv-booking_fee. + travel_id = s_edit-travel_id. + TRY. + booking_fee = s_edit-booking_fee. + CATCH cx_sy_conversion_error. + client->message_box_display( text = |Booking fee '{ s_edit-booking_fee }' is not a number - write it like 20.00| + type = `error` ). + RETURN. + ENDTRY. + + " first the UPDATE: it takes the lock of the business object, so from + " here to the COMMIT nobody else can save this travel + MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + UPDATE FIELDS ( bookingfee currencycode description ) + WITH VALUE #( ( travelid = travel_id + bookingfee = booking_fee + currencycode = s_edit-currency + description = s_edit-description ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + + IF s_failed-travel IS NOT INITIAL. + " locked by someone who is saving right now, or gone + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). + RETURN. + ENDIF. + + " then the check, against the database: the UPDATE above is still in the + " transactional buffer, so what is read here is the last saved state + SELECT SINGLE FROM z2ui5_r_smps_trv + FIELDS LastChangedAt + WHERE TravelId = @travel_id + INTO @DATA(current). + + IF sy-subrc <> 0 OR current <> s_edit-last_changed_at. + " releases the lock and drops the change - the user decides again on + " the current state rather than overwriting what someone else saved + ROLLBACK ENTITIES. + client->popup_destroy( ). + data_read( ). + view_display( ). + client->message_box_display( text = |Travel { s_edit-travel_id } was changed by someone else after you opened it. | && + `Your change was not saved - open the travel again to see the current values.` + type = `warning` ). + RETURN. + ENDIF. + + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). + + IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). + RETURN. + ENDIF. + + client->popup_destroy( ). + data_read( ). + view_display( ). + client->message_toast_display( |Travel { s_edit-travel_id } saved| ). + + ENDMETHOD. + + + METHOD travel_change_elsewhere. + + " what a second user does while the popup is open: change the same + " travel and save it. Its own roundtrip, its own COMMIT - and with it a + " new LastChangedAt, set by the managed runtime + DATA travel_id TYPE z2ui5_t_smps_trv-travel_id. + travel_id = s_edit-travel_id. + + MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + UPDATE FIELDS ( description ) + WITH VALUE #( ( travelid = travel_id + description = |Changed elsewhere at { cl_abap_context_info=>get_system_time( ) TIME = ISO }| ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + + IF s_failed-travel IS NOT INITIAL. + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). + RETURN. + ENDIF. + + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). + + IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). + RETURN. + ENDIF. + + client->message_toast_display( `Saved by someone else - now press Save` ). + + ENDMETHOD. + + + METHOD view_display. + + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `View` ns = `mvc` + )->a( n = `displayBlock` v = `true` + )->a( n = `height` v = `100%` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ). + DATA(page) = view->ele( `Shell` + )->ele( `Page` + )->a( n = `title` v = `abap2UI5 - EML - 32 Edit with Conflict Check` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + + page->tag( `MessageStrip` + )->a( n = `text` v = `Edit a travel, press Change by Someone Else in the popup, then Save - the conflict is detected ` && + `from LastChangedAt, without any lock held while you type.` + )->a( n = `type` v = `Information` + )->a( n = `showIcon` b = abap_true + )->a( n = `class` v = `sapUiSmallMargin` ). + + DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `noDataText` v = `No travels yet - press Reset Demo Data` ). + table->ele( `headerToolbar` + )->ele( `Toolbar` + )->tag( `Title` + )->a( n = `text` v = `Travels (Z2UI5_R_SMPS_TRV)` + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `GENERATE` ) + )->a( n = `text` v = `Reset Demo Data` + )->a( n = `tooltip` v = `Deletes every travel, then creates the demo travels again` + )->tag( `Button` + )->a( n = `press` v = client->_event( `REFRESH` ) + )->a( n = `icon` v = `sap-icon://refresh` + )->a( n = `tooltip` v = `Refresh` ). + + table->ele( `columns` + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Travel` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Booking Fee` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Description` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Last Changed` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `` ). + + table->ele( `items` + )->ele( `ColumnListItem` + )->ele( `cells` + )->tag( `Text` + )->a( n = `text` v = `{TRAVEL_ID}` + )->tag( `Text` + )->a( n = `text` v = `{BOOKING_FEE} {CURRENCY}` + )->tag( `Text` + )->a( n = `text` v = `{DESCRIPTION}` + )->tag( `Text` + )->a( n = `text` v = `{CHANGED_AT}` + )->tag( `Button` + )->a( n = `press` v = client->_event( val = `EDIT` arg = `${TRAVEL_ID}` ) + )->a( n = `icon` v = `sap-icon://edit` + )->a( n = `text` v = `Edit` ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. + + + METHOD popup_edit_display. + + DATA(popup) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `FragmentDefinition` ns = `core` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:core` v = `sap.ui.core` + )->a( n = `xmlns:form` v = `sap.ui.layout.form` ). + DATA(dialog) = popup->ele( `Dialog` + )->a( n = `title` t = |Edit Travel { s_edit-travel_id }| + )->a( n = `contentWidth` v = `30rem` ). + + dialog->ele( n = `SimpleForm` ns = `form` + )->a( n = `editable` b = abap_true + )->ele( n = `content` ns = `form` + )->tag( `Label` + )->a( n = `text` v = `Booking Fee` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_edit-booking_fee ) + )->tag( `Label` + )->a( n = `text` v = `Currency` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_edit-currency ) + )->tag( `Label` + )->a( n = `text` v = `Description` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_edit-description ) ). + + dialog->ele( `buttons` + )->tag( `Button` + )->a( n = `press` v = client->_event( `ELSEWHERE` ) + )->a( n = `text` v = `Change by Someone Else` + )->a( n = `tooltip` v = `Saves the travel in a roundtrip of its own, as a second user would` + )->tag( `Button` + )->a( n = `press` v = client->_event( `SAVE` ) + )->a( n = `text` v = `Save` + )->a( n = `type` v = `Emphasized` + )->tag( `Button` + )->a( n = `press` v = client->_event( `CANCEL` ) + )->a( n = `text` v = `Cancel` ). + + client->popup_display( popup->stringify( ) ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/03/z2ui5_cl_smps_app_032.clas.xml b/src/03/z2ui5_cl_smps_app_032.clas.xml new file mode 100644 index 0000000..a64e328 --- /dev/null +++ b/src/03/z2ui5_cl_smps_app_032.clas.xml @@ -0,0 +1,16 @@ + + + + + + Z2UI5_CL_SMPS_APP_032 + E + RAP - Edit with Conflict Check + 1 + X + X + X + + + + diff --git a/src/13/README.md b/src/13/README.md index a8d7ee0..6ce76b5 100644 --- a/src/13/README.md +++ b/src/13/README.md @@ -22,8 +22,8 @@ The mail sample also needs **outbound mail** configured: on ABAP Cloud a communication arrangement for the scenario `SAP_COM_0548`, on-premise SMTP in transaction `SCOT`. Without it the send fails, and the sample shows the reason. -The import sample creates travels in the business object `Z2UI5_R_SMPS_TRV` of -[`03` RAP](../03/README.md). The branch carries `src/03` along for it, so +The import and the report sample work on the travels of the business object +`Z2UI5_R_SMPS_TRV` of [`03` RAP](../03/README.md). The branch carries `src/03` along for it, so nothing has to be installed separately. ## The samples @@ -34,6 +34,7 @@ nothing has to be installed separately. | export a table as an `.xlsx` download | [`022`](z2ui5_cl_smps_app_022.clas.abap) | | send a table as an `.xlsx` mail attachment | [`023`](z2ui5_cl_smps_app_023.clas.abap) | | import the rows of an `.xlsx` file into a RAP business object | [`028`](z2ui5_cl_smps_app_028.clas.abap) | +| a report with a selection, a total and an `.xlsx` export | [`031`](z2ui5_cl_smps_app_031.clas.abap) | The XCO calls sit in [`Z2UI5_CL_SMPS_XLSX`](z2ui5_cl_smps_xlsx.clas.abap), which the three samples share: an `.xlsx` in, a table of strings out, and the way diff --git a/src/13/z2ui5_cl_smps_app_031.clas.abap b/src/13/z2ui5_cl_smps_app_031.clas.abap new file mode 100644 index 0000000..6d914b7 --- /dev/null +++ b/src/13/z2ui5_cl_smps_app_031.clas.abap @@ -0,0 +1,330 @@ +" @keywords report selection screen filter range export excel xlsx download travel sql +" @summary A report as it is written every day - a selection with ranges, the result in a table with a total, and the same rows exported as .xlsx with XCO +"!

Excel - Report with Selection and Export

+"! The classic report in three parts, each one a few lines: +"! +"! 1. the selection - agency, customer and a date window, each field turned +"! into a range table, so an empty field means "all" exactly as on a +"! selection screen +"! 2. the result - one SELECT on the CDS view entity with the ranges in its +"! WHERE, shown in a table with the number of rows and a total +"! 3. the export - the rows on the screen written into an .xlsx with the XCO +"! XLSX library and handed to the browser as a download +"! +"! SELECT FROM z2ui5_r_smps_trv FIELDS ... +"! WHERE AgencyId IN @t_agency +"! AND CustomerId IN @t_customer +"! AND BeginDate IN @t_begin +"! INTO TABLE @DATA(t_result). +"! +"! The export writes what the report shows, not a second SELECT, so the file +"! and the screen cannot disagree. Reads the travels of package 03. +CLASS z2ui5_cl_smps_app_031 DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_selection, + agency_id TYPE string, + customer_id TYPE string, + "! yyyyMMdd, the value format of the DatePicker + begin_from TYPE string, + begin_to TYPE string, + max_rows TYPE i, + END OF ty_s_selection. + DATA s_selection TYPE ty_s_selection. + + TYPES: + BEGIN OF ty_s_travel, + travel_id TYPE string, + agency_id TYPE string, + customer_id TYPE string, + begin_date TYPE string, + end_date TYPE string, + total_price TYPE string, + currency TYPE string, + status TYPE string, + END OF ty_s_travel. + TYPES ty_t_travel TYPE STANDARD TABLE OF ty_s_travel WITH EMPTY KEY. + DATA t_travels TYPE ty_t_travel. + + "! the line under the table - number of rows and the total per currency + DATA summary TYPE string. + + PROTECTED SECTION. + DATA client TYPE REF TO z2ui5_if_client. + + METHODS on_event. + METHODS data_search. + METHODS data_export. + METHODS view_display. + + PRIVATE SECTION. +ENDCLASS. + + +CLASS z2ui5_cl_smps_app_031 IMPLEMENTATION. + + METHOD z2ui5_if_app~main. + + me->client = client. + IF client->check_on_init( ). + s_selection-max_rows = 200. + data_search( ). + view_display( ). + ELSEIF client->check_on_navigated( ). + view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). + ENDIF. + + ENDMETHOD. + + + METHOD on_event. + + CASE client->get_event( ). + WHEN `SEARCH`. + data_search( ). + view_display( ). + WHEN `RESET`. + s_selection = VALUE #( max_rows = 200 ). + data_search( ). + view_display( ). + WHEN `EXPORT`. + data_export( ). + ENDCASE. + + ENDMETHOD. + + + METHOD data_search. + + TYPES ty_r_agency TYPE RANGE OF z2ui5_t_smps_trv-agency_id. + TYPES ty_r_customer TYPE RANGE OF z2ui5_t_smps_trv-customer_id. + TYPES ty_r_date TYPE RANGE OF z2ui5_t_smps_trv-begin_date. + TYPES: + BEGIN OF ty_s_total, + currency TYPE z2ui5_t_smps_trv-currency_code, + amount TYPE z2ui5_t_smps_trv-total_price, + END OF ty_s_total. + DATA t_total TYPE SORTED TABLE OF ty_s_total WITH UNIQUE KEY currency. + + IF NOT matches( val = s_selection-agency_id && s_selection-customer_id regex = `^\d*$` ). + client->message_box_display( text = `Agency and customer are numbers - leave a field empty to select all` type = `error` ). + RETURN. + ENDIF. + + " an empty range selects everything - the selection-screen behavior, + " without a branch per field + DATA(t_agency) = VALUE ty_r_agency( ). + IF s_selection-agency_id IS NOT INITIAL. + t_agency = VALUE #( ( sign = `I` option = `EQ` low = s_selection-agency_id ) ). + ENDIF. + + DATA(t_customer) = VALUE ty_r_customer( ). + IF s_selection-customer_id IS NOT INITIAL. + t_customer = VALUE #( ( sign = `I` option = `EQ` low = s_selection-customer_id ) ). + ENDIF. + + " a window open on one side is a GE or an LE, open on both no range at all + DATA(t_begin) = VALUE ty_r_date( ). + IF s_selection-begin_from IS NOT INITIAL AND s_selection-begin_to IS NOT INITIAL. + t_begin = VALUE #( ( sign = `I` option = `BT` low = s_selection-begin_from high = s_selection-begin_to ) ). + ELSEIF s_selection-begin_from IS NOT INITIAL. + t_begin = VALUE #( ( sign = `I` option = `GE` low = s_selection-begin_from ) ). + ELSEIF s_selection-begin_to IS NOT INITIAL. + t_begin = VALUE #( ( sign = `I` option = `LE` low = s_selection-begin_to ) ). + ENDIF. + + DATA(max_rows) = COND i( WHEN s_selection-max_rows > 0 THEN s_selection-max_rows ELSE 200 ). + + SELECT FROM z2ui5_r_smps_trv + FIELDS TravelId, AgencyId, CustomerId, BeginDate, EndDate, TotalPrice, CurrencyCode, OverallStatus + WHERE AgencyId IN @t_agency + AND CustomerId IN @t_customer + AND BeginDate IN @t_begin + ORDER BY BeginDate, TravelId + INTO TABLE @DATA(t_result) UP TO @max_rows ROWS. + + t_travels = VALUE #( FOR r IN t_result + ( travel_id = |{ r-travelid ALPHA = OUT }| + agency_id = |{ r-agencyid ALPHA = OUT }| + customer_id = |{ r-customerid ALPHA = OUT }| + begin_date = |{ r-begindate DATE = ISO }| + end_date = |{ r-enddate DATE = ISO }| + total_price = |{ r-totalprice }| + currency = |{ r-currencycode }| + status = z2ui5_cl_smps_context=>status_get_text( r-overallstatus ) ) ). + + " a total only makes sense per currency - summed in the types of the + " business object, not from the strings on the screen + LOOP AT t_result INTO DATA(result). + DATA(total) = VALUE ty_s_total( currency = result-currencycode amount = result-totalprice ). + COLLECT total INTO t_total. + ENDLOOP. + + summary = |{ lines( t_travels ) } travels| && + COND #( WHEN lines( t_travels ) = max_rows THEN | (the first { max_rows } - narrow the selection)| ELSE `` ) && + REDUCE string( INIT s = `` FOR t IN t_total NEXT s = |{ s }, { t-amount } { t-currency }| ). + + ENDMETHOD. + + + METHOD data_export. + + IF t_travels IS INITIAL. + client->message_toast_display( `Nothing to export - the selection found no travel` ). + RETURN. + ENDIF. + + " the rows on the screen, not a second SELECT - the file shows exactly + " what the user looked at. Six columns, A to F, as z2ui5_cl_smps_xlsx + " writes them + DATA(t_cells) = VALUE z2ui5_cl_smps_xlsx=>ty_t_row( + ( a = `Travel` b = `Customer` c = `Begin Date` d = `End Date` e = `Total Price` f = `Status` ) ). + LOOP AT t_travels INTO DATA(travel). + APPEND VALUE #( a = travel-travel_id + b = travel-customer_id + c = travel-begin_date + d = travel-end_date + e = |{ travel-total_price } { travel-currency }| + f = travel-status ) TO t_cells. + ENDLOOP. + + DATA(xlsx) = z2ui5_cl_smps_xlsx=>write( t_cells ). + DATA(file_name) = |travels_{ cl_abap_context_info=>get_system_date( ) }.xlsx|. + + client->follow_up_action( + val = client->cs_event-download_b64_file + t_arg = VALUE #( ( |data:{ z2ui5_cl_smps_xlsx=>mime_type };base64,{ cl_web_http_utility=>encode_x_base64( xlsx ) }| ) + ( file_name ) ) ). + + ENDMETHOD. + + + METHOD view_display. + + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `View` ns = `mvc` + )->a( n = `displayBlock` v = `true` + )->a( n = `height` v = `100%` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` + )->a( n = `xmlns:form` v = `sap.ui.layout.form` ). + DATA(page) = view->ele( `Shell` + )->ele( `Page` + )->a( n = `title` v = `abap2UI5 - Excel - 31 Report with Selection and Export` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + + page->ele( n = `SimpleForm` ns = `form` + )->a( n = `title` v = `Selection` + )->a( n = `editable` b = abap_true + )->ele( n = `content` ns = `form` + )->tag( `Label` + )->a( n = `text` v = `Agency` + )->tag( `Input` + )->a( n = `placeholder` v = `all` + )->a( n = `value` v = client->_bind( s_selection-agency_id ) + )->a( n = `submit` v = client->_event( `SEARCH` ) + )->tag( `Label` + )->a( n = `text` v = `Customer` + )->tag( `Input` + )->a( n = `placeholder` v = `all` + )->a( n = `value` v = client->_bind( s_selection-customer_id ) + )->a( n = `submit` v = client->_event( `SEARCH` ) + )->tag( `Label` + )->a( n = `text` v = `Begin date from / to` + )->tag( `DatePicker` + )->a( n = `value` v = client->_bind( s_selection-begin_from ) + )->a( n = `valueFormat` v = `yyyyMMdd` + )->tag( `DatePicker` + )->a( n = `value` v = client->_bind( s_selection-begin_to ) + )->a( n = `valueFormat` v = `yyyyMMdd` + )->tag( `Label` + )->a( n = `text` v = `Maximum rows` + )->tag( `StepInput` + )->a( n = `value` v = client->_bind( s_selection-max_rows ) + )->a( n = `min` v = `1` + )->a( n = `max` v = `5000` + )->a( n = `step` v = `100` + )->tag( `Label` + )->ele( `HBox` + )->tag( `Button` + )->a( n = `press` v = client->_event( `SEARCH` ) + )->a( n = `text` v = `Search` + )->a( n = `type` v = `Emphasized` + )->a( n = `icon` v = `sap-icon://search` + )->tag( `Button` + )->a( n = `press` v = client->_event( `RESET` ) + )->a( n = `text` v = `Reset` + )->a( n = `class` v = `sapUiTinyMarginBegin` ). + + DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `noDataText` v = `No travel matches the selection` ). + table->ele( `headerToolbar` + )->ele( `Toolbar` + )->tag( `Title` + )->a( n = `text` v = client->_bind( summary ) + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `EXPORT` ) + )->a( n = `text` v = `Export to Excel` + )->a( n = `icon` v = `sap-icon://excel-attachment` ). + + table->ele( `columns` + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Travel` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Agency` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Customer` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Begin Date` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `End Date` + )->end( + )->ele( `Column` + )->a( n = `hAlign` v = `End` + )->tag( `Text` + )->a( n = `text` v = `Total Price` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `Status` ). + + table->ele( `items` + )->ele( `ColumnListItem` + )->ele( `cells` + )->tag( `Text` + )->a( n = `text` v = `{TRAVEL_ID}` + )->tag( `Text` + )->a( n = `text` v = `{AGENCY_ID}` + )->tag( `Text` + )->a( n = `text` v = `{CUSTOMER_ID}` + )->tag( `Text` + )->a( n = `text` v = `{BEGIN_DATE}` + )->tag( `Text` + )->a( n = `text` v = `{END_DATE}` + )->tag( `Text` + )->a( n = `text` v = `{TOTAL_PRICE} {CURRENCY}` + )->tag( `Text` + )->a( n = `text` v = `{STATUS}` ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/13/z2ui5_cl_smps_app_031.clas.xml b/src/13/z2ui5_cl_smps_app_031.clas.xml new file mode 100644 index 0000000..bf87141 --- /dev/null +++ b/src/13/z2ui5_cl_smps_app_031.clas.xml @@ -0,0 +1,16 @@ + + + + + + Z2UI5_CL_SMPS_APP_031 + E + Excel - Report with Selection and Export + 1 + X + X + X + + + + diff --git a/src/z2ui5_cl_smps_app_000.clas.abap b/src/z2ui5_cl_smps_app_000.clas.abap index 92c4ed1..2bb1184 100644 --- a/src/z2ui5_cl_smps_app_000.clas.abap +++ b/src/z2ui5_cl_smps_app_000.clas.abap @@ -976,7 +976,19 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. ( sample( no = `005` title = `Manage travels - the complete app` detail = `01-04 plus EXECUTE and COMMIT ENTITIES RESPONSE OF` - classname = `Z2UI5_CL_SMPS_APP_005` ) ) ). + classname = `Z2UI5_CL_SMPS_APP_005` ) ) + ( sample( no = `029` + title = `Mass change with preview` + detail = `Several travels changed in one go - the new values are previewed next to the old ones, one MODIFY ENTITIES ... UPDATE saves them, and a refusal is shown on its row` + classname = `Z2UI5_CL_SMPS_APP_029` ) ) + ( sample( no = `030` + title = `Approval worklist` + detail = `An approval worklist - the open travels, several selected and accepted or rejected with one EXECUTE, the list shrinking as they are decided` + classname = `Z2UI5_CL_SMPS_APP_030` ) ) + ( sample( no = `032` + title = `Edit with conflict check` + detail = `Edit a travel without a stateful session - the app remembers LastChangedAt when it reads, and Save refuses the change if someone else saved the travel in between` + classname = `Z2UI5_CL_SMPS_APP_032` ) ) ). t_draft = VALUE #( ( sample( no = `006` @@ -1120,7 +1132,11 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. ( sample( no = `028` title = `Import an XLSX file into a RAP business object` detail = `An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to` - classname = `Z2UI5_CL_SMPS_APP_028` ) ) ). + classname = `Z2UI5_CL_SMPS_APP_028` ) ) + ( sample( no = `031` + title = `Report with selection and export` + detail = `A report as it is written every day - a selection with ranges, the result in a table with a total, and the same rows exported as .xlsx with XCO` + classname = `Z2UI5_CL_SMPS_APP_031` ) ) ). t_forms = VALUE #( ( sample( no = `024`