From 3d2ad1bf124a06f12d187e1b419d75dc8239db69 Mon Sep 17 00:00:00 2001 From: Mark Williams Date: Tue, 9 Jun 2026 16:54:44 +0100 Subject: [PATCH 1/2] LIMS-2205: Allow incoming EU shipments to be booked via the shipping service --- api/config_sample.php | 1 + api/index.php | 3 +- api/src/Page/Shipment.php | 38 ++++++++++------- .../js/modules/shipment/views/createawb.js | 3 +- .../src/js/modules/shipment/views/shipment.js | 42 ++++++++++++++----- .../js/modules/shipment/views/shipmentadd.js | 27 +++++++++++- .../js/templates/shipment/shipmentadd.html | 2 +- 7 files changed, 86 insertions(+), 30 deletions(-) diff --git a/api/config_sample.php b/api/config_sample.php index e89e01bec..7bcbfb406 100644 --- a/api/config_sample.php +++ b/api/config_sample.php @@ -266,6 +266,7 @@ $use_shipping_service = null; $use_shipping_service_nde = False; $use_shipping_service_incoming_shipments = null; + $use_shipping_service_incoming_shipments_nde = False; $use_shipping_service_redirect = null; $use_shipping_service_redirect_incoming_shipments = null; $shipping_service_api_user = null; diff --git a/api/index.php b/api/index.php index f4116e816..96e785b5c 100644 --- a/api/index.php +++ b/api/index.php @@ -72,7 +72,7 @@ function setupApplication($mode): Slim $visit_persist_storage_dir_segment, $dhl_enable, $scale_grid, $scale_grid_end_date, $preset_proposal, $timezone, $valid_components, $enabled_container_types, $synchweb_version, $redirects, $shipping_service_app_url, $use_shipping_service_redirect, $use_shipping_service_redirect_incoming_shipments, - $use_shipping_service_nde, + $use_shipping_service_nde, $use_shipping_service_incoming_shipments_nde, $dials_rest_url_rings, $closed_proposal_link, $ccp4_cloud_upload_url, $only_staff_can_assign, $industrial_prop_codes, $upstream_reprocessing_pipelines, $downstream_reprocessing_pipelines, $prop_codes_data_deleted, $container_types_with_parents, $bl_capacity; @@ -103,6 +103,7 @@ function setupApplication($mode): Slim 'shipping_service_app_url' => $use_shipping_service_redirect ? $shipping_service_app_url : null, 'shipping_service_app_url_incoming' => $use_shipping_service_redirect_incoming_shipments ? $shipping_service_app_url : null, 'use_shipping_service_nde' => $use_shipping_service_nde, + 'use_shipping_service_incoming_shipments_nde' => $use_shipping_service_incoming_shipments_nde, 'dials_rest_url_rings' => $dials_rest_url_rings, 'closed_proposal_link' => $closed_proposal_link, 'ccp4_cloud_upload_url' => $ccp4_cloud_upload_url, diff --git a/api/src/Page/Shipment.php b/api/src/Page/Shipment.php index 63141cef8..00d42d596 100644 --- a/api/src/Page/Shipment.php +++ b/api/src/Page/Shipment.php @@ -1098,19 +1098,23 @@ function _dispatch_dewar_shipment_request($dewar) return $dewar['EXTERNALSHIPPINGIDFROMSYNCHROTRON']; } - $dewars = [$dewar]; - $proposal = $dewar['PROPOSAL']; - $session_number = $dewar['VIS']; - $external_id = (int) $dewar['DEWARID']; - $shipping_id = (int) $dewar['SHIPPINGID']; - $token = Utils::generateRandomMd5(); - $this->db->pq( - "UPDATE dewar SET extra = JSON_SET(IFNULL(extra, '{}'), '$.token', :1 ) WHERE dewarid=:2", - array($token, $external_id) - ); + if (!is_null($dewar['EXTERNALSHIPPINGIDTOSYNCHROTRON']) && !is_null($dewar['TOKEN']) && $dewar['DCOUNT'] == 1) { + $external_shipping_id = $dewar['EXTERNALSHIPPINGIDTOSYNCHROTRON']; + } else { + $dewars = [$dewar]; + $proposal = $dewar['PROPOSAL']; + $session_number = $dewar['VIS']; + $external_id = (int) $dewar['DEWARID']; + $shipping_id = (int) $dewar['SHIPPINGID']; + $token = Utils::generateRandomMd5(); + $this->db->pq( + "UPDATE dewar SET extra = JSON_SET(IFNULL(extra, '{}'), '$.token', :1 ) WHERE dewarid=:2", + array($token, $external_id) + ); - $callback_url = "/api/shipment/dewars/confirmdispatch/did/{$external_id}/token/{$token}"; - $external_shipping_id = $this->_create_dewars_shipment_request($dewars, $proposal, $session_number, $external_id, $shipping_id, $callback_url); + $callback_url = "/api/shipment/dewars/confirmdispatch/did/{$external_id}/token/{$token}"; + $external_shipping_id = $this->_create_dewars_shipment_request($dewars, $proposal, $session_number, $external_id, $shipping_id, $callback_url); + } $this->db->pq( "UPDATE dewar SET externalShippingIdFromSynchrotron=:1 WHERE dewarid=:2", @@ -1214,7 +1218,9 @@ function _dispatch_dewar() $dew = $this->db->pq( "SELECT d.dewarid, d.barcode, d.storagelocation, d.dewarstatus, d.externalShippingIdFromSynchrotron, + s.externalshippingidtosynchrotron, json_unquote(json_extract(d.extra, '$.token')) as token, s.shippingid, p.proposalcode, CONCAT(p.proposalcode, p.proposalnumber) as proposal, + (SELECT COUNT(*) FROM dewar d2 WHERE d2.shippingid = s.shippingid) as dcount, count(distinct c.containerId) as num_pucks, count(b.blsampleId) as num_samples, ifnull(bls.visit_number, 0) as vis, IF(d.facilitycode, d.facilitycode, d.code) as name, dr.manufacturerserialnumber @@ -3544,7 +3550,7 @@ function _create_awb() { global $dhl_service, $dhl_service_eu, $dhl_acc, $dhl_acc_import, $facility_courier_countries, $facility_courier_countries_nde, $use_shipping_service_incoming_shipments, - $use_shipping_service_redirect_incoming_shipments; + $use_shipping_service_incoming_shipments_nde, $use_shipping_service_redirect_incoming_shipments; if (!$this->has_arg('prop')) $this->_error('No proposal specified'); if (!$this->has_arg('sid')) @@ -3591,8 +3597,10 @@ function _create_awb() } if ( - Utils::getValueOrDefault($use_shipping_service_incoming_shipments) - && in_array($this->arg('COUNTRY'), $facility_courier_countries) + ((Utils::getValueOrDefault($use_shipping_service_incoming_shipments) + && in_array($this->arg('COUNTRY'), $facility_courier_countries)) + || (Utils::getValueOrDefault($use_shipping_service_incoming_shipments_nde) + && in_array($this->arg('COUNTRY'), $facility_courier_countries_nde))) && Utils::getValueOrDefault($use_shipping_service_redirect_incoming_shipments) ) { if ($ship['EXTERNALSHIPPINGIDTOSYNCHROTRON']) { diff --git a/client/src/js/modules/shipment/views/createawb.js b/client/src/js/modules/shipment/views/createawb.js index 9d5373bb9..9f11ba4a9 100644 --- a/client/src/js/modules/shipment/views/createawb.js +++ b/client/src/js/modules/shipment/views/createawb.js @@ -520,7 +520,8 @@ define(['backbone', if ( ss_url && (Number(self.terms.get('ACCEPTED')) === 1) // terms.ACCEPTED could be undefined, 1, or "1" - && app.options.get("facility_courier_countries").includes(country) + && (app.options.get("facility_courier_countries").includes(country) || + app.options.get("facility_courier_countries_nde").includes(country)) ) { self.shipment.fetch().done((shipment) => { const external_id = shipment.EXTERNALSHIPPINGIDTOSYNCHROTRON; diff --git a/client/src/js/modules/shipment/views/shipment.js b/client/src/js/modules/shipment/views/shipment.js index 394e03907..0129dc56c 100644 --- a/client/src/js/modules/shipment/views/shipment.js +++ b/client/src/js/modules/shipment/views/shipment.js @@ -246,6 +246,7 @@ define(['marionette', const safetylevel = this.model.get('SAFETYLEVEL') const externalid = this.model.get('EXTERNALSHIPPINGIDTOSYNCHROTRON') const shippingid = this.model.get('SHIPPINGID') + const dcount = this.model.get('DCOUNT') if (!courier || (app.options.get('dhl_enable') && courier.toLowerCase().trim() == 'dhl')) { if (label == '1') { this.ui.booking.html(' Print Air Waybill') @@ -253,11 +254,17 @@ define(['marionette', if (!pickup) { this.ui.booking.append(' Rebook Pickup') } - } else if ((country && !fac_country.includes(country)) || safetylevel == "Red") { - this.ui.booking.html(' Create Air Waybill - Disabled') } else if (externalid && ss_url) { const link = ss_url+'/shipment-requests/'+externalid+'/incoming' this.ui.booking.html(' Manage Shipment Booking') + } else if (safetylevel && safetylevel == "Red") { + this.ui.booking.html(' Create Air Waybill - Disabled') + } else if (this.nde && (this.industrial_visit || dcount > 1 || !this.ss_nde_incoming)) { + this.ui.booking.html(' Create Air Waybill - Disabled') + } else if (this.nde) { + this.ui.booking.html(' Request DHL Air Waybill') + } else if (country && !fac_country.includes(country)) { + this.ui.booking.html(' Create Air Waybill - Disabled') } else { this.ui.booking.html(' Create DHL Air Waybill') } @@ -272,23 +279,26 @@ define(['marionette', const safetylevel = this.model.get('SAFETYLEVEL') const country = this.model.get('COUNTRY') const courier = this.model.get('DELIVERYAGENT_AGENTNAME') - const fac_country_nde = app.options.get('facility_courier_countries_nde') - const fac_country_link = app.options.get('facility_courier_countries_link') const fac_country = app.options.get('facility_courier_countries') const ss_url = app.options.get("shipping_service_app_url_incoming") const externalid = this.model.get('EXTERNALSHIPPINGIDTOSYNCHROTRON') + const dcount = this.model.get('DCOUNT') if (label == '1') { this.ui.dhlmessage.html('

You can print your Air Waybill by clicking "Print Air Waybill" below.

') + } else if (externalid && ss_url) { + this.ui.dhlmessage.html('

You can now manage your shipment with DHL using "Manage Shipment Booking" below.

') } else if (safetylevel && safetylevel == "Red") { this.ui.dhlmessage.html('

Shipping of red samples is not available through this application.

') - } else if (country && fac_country_nde.includes(country) ) { - this.ui.dhlmessage.html('

International shipping is not available through this application. If you're arranging your own shipping (e.g. industrial users), enter your tracking numbers below after booking and include printed return labels in the dewar case. European academic users, please see here.

') + } else if (this.nde && (this.industrial_visit || !this.ss_nde_incoming)) { + this.ui.dhlmessage.html('

International shipping is not available through this application. If you're arranging your own shipping, enter your tracking numbers below after booking and include printed return labels in the dewar case.

') + } else if (this.nde && dcount > 1) { + this.ui.dhlmessage.html('

International shipping is not available where there is more than one dewar in the shipment

') + } else if (this.nde) { + this.ui.dhlmessage.html('

You can now request your shipment with DHL using "Request Air Waybill" below.

') } else if (country && !fac_country.includes(country) ) { this.ui.dhlmessage.html('

International shipping is not available through this application. If you're arranging your own shipping, enter your tracking numbers below after booking and include printed return labels in the dewar case.

') } else if (courier && courier.toLowerCase().trim() != 'dhl') { this.ui.dhlmessage.html('

Shipping through this application is only available using DHL.

') - } else if (externalid && ss_url) { - this.ui.dhlmessage.html('

You can now manage your shipment with DHL using "Manage Shipment Booking" below.

') } else { this.ui.dhlmessage.html('

You can now book your shipment with DHL using "Create Air Waybill" below.

') } @@ -324,7 +334,12 @@ define(['marionette', this.$el.find(".remoteform").show() } } - if (dynamic === 'Ready' || (app.proposal && app.proposal.get('ACTIVE') != '1')) { + + if ( + dynamic === 'Ready' || + (app.proposal && app.proposal.get('ACTIVE') != '1') || + (this.termsaccepted && this.nde && this.ss_nde_incoming) + ) { this.ui.add_dewar.hide() } else { this.ui.add_dewar.show() @@ -393,6 +408,12 @@ define(['marionette', this.edit.create("ONSITEUSERS", 'text', { placeholder: 'No' }); this.edit.create("MULTIAXISGONIOMETRY", 'select', { data: {'Yes': 'Yes', 'No': 'No'}}) + this.termsaccepted = this.model.get('TERMSACCEPTED') == 1 + const country = this.model.get('COUNTRY') + const fac_country_nde = app.options.get('facility_courier_countries_nde') + this.ss_nde_incoming = app.options.get('use_shipping_service_incoming_shipments_nde') + this.nde = country && fac_country_nde.includes(country) + this.updateGUI() this.listenTo(this.model, "change", this.updateGUI) @@ -406,9 +427,8 @@ define(['marionette', }) const externalid = this.model.get('EXTERNALSHIPPINGIDTOSYNCHROTRON') - const termsaccepted = this.model.get('TERMSACCEPTED') == 1 const ss_url = app.options.get("shipping_service_app_url_incoming") - if (ss_url && (externalid || termsaccepted)) { + if (ss_url && (externalid || this.termsaccepted)) { this.ui.courierfields.hide() } }, diff --git a/client/src/js/modules/shipment/views/shipmentadd.js b/client/src/js/modules/shipment/views/shipmentadd.js index 9c0d36894..2b31396cf 100644 --- a/client/src/js/modules/shipment/views/shipmentadd.js +++ b/client/src/js/modules/shipment/views/shipmentadd.js @@ -130,6 +130,12 @@ define(['marionette', 'views/form', this.model.set({ TERMSACCEPTED: 1 }) this.ui.accountnumber.val('') this.ui.couriername.val('') + const lc = this.contacts.findWhere({ LABCONTACTID: this.ui.lcout.val() }) + const nde = app.options.get('facility_courier_countries_nde').indexOf(lc.get('COUNTRY')) > -1 + const ss_nde_incoming = app.options.get('use_shipping_service_incoming_shipments_nde') + if (nde && ss_nde_incoming) { + this.setMaxDewarsPerShipment() + } }, addLC: function(e) { @@ -326,9 +332,12 @@ define(['marionette', 'views/form', const industrial_visit = industrial_codes.includes(app.prop.slice(0,2)) const lc = this.contacts.findWhere({ LABCONTACTID: this.ui.lcout.val() }) - const countryOk = app.options.get('facility_courier_countries').indexOf(lc.get('COUNTRY')) > -1 || app.options.get('facility_courier_countries_nde').indexOf(lc.get('COUNTRY')) > -1 + const domestic = app.options.get('facility_courier_countries').indexOf(lc.get('COUNTRY')) > -1 + const nde = app.options.get('facility_courier_countries_nde').indexOf(lc.get('COUNTRY')) > -1 + const countryOk = domestic || nde const usingFacilityAccount = this.model.get('TERMSACCEPTED') === 1 + const ss_nde_incoming = app.options.get('use_shipping_service_incoming_shipments_nde') if (countryOk && !industrial_visit && !usingFacilityAccount) { this.ui.facc.show() @@ -341,6 +350,22 @@ define(['marionette', 'views/form', this.ui.accmsg.text('') this.model.set({ TERMSACCEPTED: 0 }) } + + if (nde && ss_nde_incoming && usingFacilityAccount) { + this.setMaxDewarsPerShipment() + } else { + this.ui.dewars.prop('disabled', false) + } + + }, + + setMaxDewarsPerShipment: function() { + if (this.ui.dewars.val() > 1) { + this.ui.dewars.val(1) + this.updateFCodes() + app.alert({ message: 'For international shipping we are limited to 1 dewar per shipment'}) + } + this.ui.dewars.prop('disabled', true) }, diff --git a/client/src/js/templates/shipment/shipmentadd.html b/client/src/js/templates/shipment/shipmentadd.html index e0fac3873..2d8d7f27d 100644 --- a/client/src/js/templates/shipment/shipmentadd.html +++ b/client/src/js/templates/shipment/shipmentadd.html @@ -22,7 +22,7 @@

Add New Shipment

- +
  • From 5ffb8f4b981a491f36ba88225b3eb687b3fc42d5 Mon Sep 17 00:00:00 2001 From: Mark Williams Date: Mon, 22 Jun 2026 13:18:54 +0100 Subject: [PATCH 2/2] LIMS-2205: Detect new country if lab contact changed --- .../src/js/modules/shipment/views/shipment.js | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/src/js/modules/shipment/views/shipment.js b/client/src/js/modules/shipment/views/shipment.js index 0129dc56c..2f4ba05be 100644 --- a/client/src/js/modules/shipment/views/shipment.js +++ b/client/src/js/modules/shipment/views/shipment.js @@ -225,7 +225,12 @@ define(['marionette', const sendingId = this.model.get('SENDINGLABCONTACTID') if (!sendingId || !this.contacts || !this.contacts.length) return const contact = this.contacts.findWhere({ LABCONTACTID: sendingId }) - if (contact) this.model.set('COUNTRY', contact.get('COUNTRY')) + if (contact) { + const country = contact.get('COUNTRY') + this.model.set('COUNTRY', country) + const fac_country_nde = app.options.get('facility_courier_countries_nde') + this.nde = country && fac_country_nde.includes(country) + } }, showButtons: function() { @@ -409,21 +414,15 @@ define(['marionette', this.edit.create("MULTIAXISGONIOMETRY", 'select', { data: {'Yes': 'Yes', 'No': 'No'}}) this.termsaccepted = this.model.get('TERMSACCEPTED') == 1 - const country = this.model.get('COUNTRY') - const fac_country_nde = app.options.get('facility_courier_countries_nde') this.ss_nde_incoming = app.options.get('use_shipping_service_incoming_shipments_nde') - this.nde = country && fac_country_nde.includes(country) - this.updateGUI() this.listenTo(this.model, "change", this.updateGUI) - - var self = this this.contacts = new LabContacts(null, { state: { pageSize: 9999 } }) - this.contacts.fetch().done(function() { - console.log(self.contacts, self.contacts.kv()) - self.edit.create('SENDINGLABCONTACTID', 'select', { data: self.contacts.kv() }) - self.edit.create('RETURNLABCONTACTID', 'select', { data: self.contacts.kv() }) + this.contacts.fetch().done(() => { + this.edit.create('SENDINGLABCONTACTID', 'select', { data: this.contacts.kv() }) + this.edit.create('RETURNLABCONTACTID', 'select', { data: this.contacts.kv() }) + this.updateGUI() }) const externalid = this.model.get('EXTERNALSHIPPINGIDTOSYNCHROTRON')