diff --git a/client/src/i18n/en/form.json b/client/src/i18n/en/form.json index ce1afb8143..724f6afd2a 100644 --- a/client/src/i18n/en/form.json +++ b/client/src/i18n/en/form.json @@ -881,6 +881,7 @@ }, "WEEKEND_CONFIGURATION" : "Weekend Configuration", "WEIGHT": "Weight", + "TOTAL_WEIGHT": "Total Weight", "WIDTH": "Width", "YES": "Yes", "YOB": "Year of Birth", diff --git a/client/src/i18n/en/shipment.json b/client/src/i18n/en/shipment.json index 813a8f7f34..548152df5c 100644 --- a/client/src/i18n/en/shipment.json +++ b/client/src/i18n/en/shipment.json @@ -28,12 +28,16 @@ "SACK": "sack", "SET": "set" }, + "CONTAINER_WEIGHT": "Weight (container only, not contents)", "CREATE": "Create", "CREATED": "Shipment successfully created", "CREATED_BY": "Created by", "CURRENT_DEPOT": "Current depot", "CURRENT_LOCATION": "Current location", "CURRENT_STATUS": "Current status", + "CONTAINER_WEIGHT_EMPTY": "Container Weight (empty)", + "CONTAINER_WEIGHT_PACKED": "Container Weight (packed)", + "CONTAINER_VALUE": "Container Value", "DATE_DELIVERED": "Date delivered", "DATE_PACKED": "Date packed", "DATE_SENT": "Date sent", @@ -41,12 +45,14 @@ "DELETED": "Shipment successfully deleted", "DELIVERY_EXPECTED_ON": "Delivery expected on", "DESTINATION_DEPOT": "Destination depot", + "DOCUMENT": "Document", "DOES_SHIPMENT_EXIST": "Do you have a shipment ready?", "DOES_SHIPMENT_EXIST_HELP": "Please provide the reference of a shipment that is ready to ship.", "DUELY_RECEIVED_BY": "Duely received by", "EDIT": "Edit", "EDIT_CONTAINER": "Edit Container", "EDIT_SHIPMENT": "Edit Shipment", + "EMPTY_WEIGHT": "Empty weight", "ENTER_SHIPMENT_PROGRESS": "Please enter the shipment progress status. Include the current shipment location if it has changed.", "ENTER_SHIPMENT_PROGRESS_HELP_TEXT": "Ex. RECEPTION - IMA GOMA WAREHOUSE, GOMA", "ENTER_STOCK_FOR_SHIPMENT" : "Enter stock for this shipment", @@ -55,8 +61,10 @@ "EXISTING_SHIPMENT_AT_DEPOT": "Current shipment in the depot ({{number}})", "EXIT_STOCK_FOR_SHIPMENT" : "Exit stock for this shipment", "IS_IN_TRANSIT_TOOLTIP": "This inventory is in transit to the depot ({{depot}})", + "ITEMS": "Item(s)", "LOCATION": "Location", "LOT_CONDITION": "Condition", + "MANIFEST": "Manifest", "MARKED_DELIVERED": "Shipment marked delivered", "MARK_DELIVERED": "Mark as delivered", @@ -78,8 +86,9 @@ "NEW_LOCATION": "New Location", "NEW_SHIPMENT": "New Shipment", "NOT_DEFINED": "Not Defined", + "NUM_ITEMS": "Num. Items", "ORIGIN_DEPOT": "Origin depot", - "OVERVIEW": "Shipment Overview", + "OVERVIEW": "Overview", "PACKING": "Packing", "PACKING_LIST": "Packing List", "PACKING_LIST_UPDATED": "Packing list updated", @@ -93,6 +102,8 @@ "SHIPMENTS": "Shipments", "SHIPMENT_CREATED": "Shipment created", "SHIPMENT_DATE": "Expedition", + "SHIPMENT_DOCUMENT": "Shipment Document", + "SHIPMENT_MANIFEST": "Shipment Manifest", "SHIPMENT_LOCATION_TRACKING": "Locations Tracking", "SHIPMENT_REFERENCE": "Shipment Reference", "SHIPMENT_REGISTRY": "Shipment Registry", @@ -105,8 +116,9 @@ "TITLE" : "Shipment", "TOTAL_COST": "Total cost", "TOTAL_QUANTITY": "Total quantity", + "TOTAL_VALUE": "Total Value", "TOTAL_WEIGHT": "Total weight", - "TRACKING_LOG": "Tracking Log", + "TRACKING_LOG": "Update Tracking Log", "TRANSPORTER": "Transporter", "TRANSPORT_MODE": "Mode of Transport", "UPDATED": "Shipment successfully updated", diff --git a/client/src/i18n/fr/form.json b/client/src/i18n/fr/form.json index 2f4686e1c9..56aa686229 100644 --- a/client/src/i18n/fr/form.json +++ b/client/src/i18n/fr/form.json @@ -809,6 +809,7 @@ "TOTAL_PAID": "Total payé", "TOTAL_SELECTED_INVOICES": "Total des factures selectionnées", "TOTAL_SELECTED_SALARIES": "Total des salaires selectionnés", + "TOTAL_WEIGHT": "Poids total", "TOTAL": "Total", "TRANSACTION_DATE": "Date de Transaction", "TRANSACTION": "Transaction", diff --git a/client/src/i18n/fr/shipment.json b/client/src/i18n/fr/shipment.json index daf08cfd3f..b24c5cb1fc 100644 --- a/client/src/i18n/fr/shipment.json +++ b/client/src/i18n/fr/shipment.json @@ -28,12 +28,16 @@ "SACK": "sac", "SET": "jeu" }, + "CONTAINER_WEIGHT": "Poids (conteneur seulement, pas le contenu)", "CREATE": "Créer", "CREATED": "Expédition créee avec succès", "CREATED_BY": "Crée par", "CURRENT_DEPOT": "Dépôt en cours", "CURRENT_LOCATION": "Localisation en cours", "CURRENT_STATUS": "Etat actuel", + "CONTAINER_WEIGHT_EMPTY": "Poids du conteneur (vide)", + "CONTAINER_WEIGHT_PACKED": "Poids du conteneur (emballé)", + "CONTAINER_VALUE": "Valeur du conteneur", "DATE_DELIVERED": "Date de livraison", "DATE_PACKED": "Date colisage", "DATE_SENT": "Date d'envoi", @@ -41,12 +45,14 @@ "DELETED": "Expédition supprimée avec succès", "DELIVERY_EXPECTED_ON": "Livraison attendue le", "DESTINATION_DEPOT": "Dépôt de destination", + "DOCUMENT": "Document d'expédition", "DOES_SHIPMENT_EXIST": "Avez-vous une expédition prête ?", "DOES_SHIPMENT_EXIST_HELP": "Veuillez renseigner la référence de l'expédition prête", "DUELY_RECEIVED_BY": "Pour réception conformé", "EDIT": "Editer", "EDIT_CONTAINER": "Modifier conteneur", "EDIT_SHIPMENT": "Modifier expédition", + "EMPTY_WEIGHT": "Poids à vide", "ENTER_SHIPMENT_PROGRESS": "Veuillez entrer l'etat de progression de l'expédition avec sa localisation actuelle", "ENTER_SHIPMENT_PROGRESS_HELP_TEXT": "Ex. RECEPTION - IMA GOMA WAREHOUSE, GOMA", "ENTER_STOCK_FOR_SHIPMENT" : "Entrer le stock pour cette expédition", @@ -55,8 +61,10 @@ "EXISTING_SHIPMENT_AT_DEPOT": "Les expéditions en cours dans le dépôt ({{number}})", "EXIT_STOCK_FOR_SHIPMENT" : "Sortir du stock pour cette expédition", "IS_IN_TRANSIT_TOOLTIP": "Cet inventaire est en transit vers le depot ({{depot}})", + "ITEMS": "Article(s)", "LOCATION": "Localisation", "LOT_CONDITION": "Condition", + "MANIFEST": "Manifeste", "MARKED_DELIVERED": "Expédition marquée livrée", "MARK_DELIVERED": "Marquer comme livré", @@ -78,8 +86,9 @@ "NEW_LOCATION": "Nouvelle localisation", "NEW_SHIPMENT": "Nouvelle expédition", "NOT_DEFINED": "Non-défini", + "NUM_ITEMS": "N. des items", "ORIGIN_DEPOT": "Dépôt d'origine", - "OVERVIEW": "Aperçu de l'expédition", + "OVERVIEW": "Aperçu", "PACKING": "Colisage", "PACKING_LIST": "Liste de colisage", "PACKING_LIST_UPDATED": "Liste de colisage mise à jour", @@ -93,6 +102,8 @@ "SHIPMENTS": "Expéditions", "SHIPMENT_CREATED": "Expédition créée", "SHIPMENT_DATE": "Date de l'expédition", + "SHIPMENT_DOCUMENT": "Document d'expédition", + "SHIPMENT_MANIFEST": "Manifeste d'expédition", "SHIPMENT_LOCATION_TRACKING": "Suivi des emplacements", "SHIPMENT_REFERENCE": "Référence Expédition", "SHIPMENT_REGISTRY": "Registre des expéditions", @@ -105,8 +116,9 @@ "TITLE" : "Expedition", "TOTAL_COST": "Côut total", "TOTAL_QUANTITY": "Quantité totale", + "TOTAL_VALUE": "Valeur totale", "TOTAL_WEIGHT": "Poids total", - "TRACKING_LOG": "Journal de suivi", + "TRACKING_LOG": "Mettre à jour le journal de suivi", "TRANSPORTER": "Transporteur", "TRANSPORT_MODE": "Mode de transport", "UPDATED": "Expédition mis à jour avec succès", diff --git a/client/src/js/services/receipts/ReceiptService.js b/client/src/js/services/receipts/ReceiptService.js index 4977e63b8f..35361eae54 100644 --- a/client/src/js/services/receipts/ReceiptService.js +++ b/client/src/js/services/receipts/ReceiptService.js @@ -52,7 +52,8 @@ function ReceiptService($http, util, Language, AppCache, Session) { service.payrollReport = payrollReport; service.displayData = displayData; - service.shipmentOverview = shipmentOverview; + service.shipmentDocument = shipmentDocument; + service.shipmentManifest = shipmentManifest; service.shipmentBarcode = shipmentBarcode; /** @@ -171,10 +172,17 @@ function ReceiptService($http, util, Language, AppCache, Session) { return fetch(route, options); } - // shipment overview - function shipmentOverview(uuid, options) { + // shipment document + function shipmentDocument(uuid, options) { options.posReceipt = service.posReceipt; - const route = `/reports/shipments/${uuid}/overview`; + const route = `/reports/shipments/${uuid}/document`; + return fetch(route, options); + } + + // shipment manifest + function shipmentManifest(uuid, options) { + options.posReceipt = service.posReceipt; + const route = `/reports/shipments/${uuid}/document?manifest=1`; return fetch(route, options); } diff --git a/client/src/modules/inventory/inventory.service.js b/client/src/modules/inventory/inventory.service.js index ddc36bc25d..b046a2f134 100644 --- a/client/src/modules/inventory/inventory.service.js +++ b/client/src/modules/inventory/inventory.service.js @@ -211,6 +211,7 @@ function InventoryService( function remove(uuid) { return service.$http.delete('/inventory/metadata/'.concat(uuid)); } + service.columnsMap = (key) => { const cols = { code : 'FORM.LABELS.CODE', @@ -229,6 +230,9 @@ function InventoryService( inventoryUnit : 'FORM.LABELS.UNIT', unit_volume : 'FORM.LABELS.VOLUME', unit_weight : 'FORM.LABELS.WEIGHT', + is_asset : 'FORM.LABELS.ASSET', + manufacturer_brand : 'FORM.LABELS.MANUFACTURER_BRAND', + manufacturer_model : 'FORM.LABELS.MANUFACTURER_MODEL', }; return cols[key] || key; diff --git a/client/src/modules/required_inventory_scans/required-inventory-scans-registry.js b/client/src/modules/required_inventory_scans/required-inventory-scans-registry.js index be8725246c..5578d727b4 100644 --- a/client/src/modules/required_inventory_scans/required-inventory-scans-registry.js +++ b/client/src/modules/required_inventory_scans/required-inventory-scans-registry.js @@ -5,7 +5,7 @@ RequiredInventoryScansRegistryController.$inject = [ 'RequiredInventoryScansService', 'RequiredInventoryScansRegistryService', 'StockModalService', 'DepotService', 'GridStateService', 'GridColumnService', 'GridGroupingService', - 'NotifyService', '$state', + 'NotifyService', '$state', '$translate', ]; /** @@ -16,7 +16,7 @@ function RequiredInventoryScansRegistryController( RequiredInventoryScans, ReqInvScansRegistryService, StockModal, Depots, GridState, Columns, Grouping, - Notify, $state, + Notify, $state, $translate, ) { const vm = this; const cacheKey = 'required-inventory-scans-grid'; @@ -26,6 +26,7 @@ function RequiredInventoryScansRegistryController( appScopeProvider : vm, enableColumnMenus : false, columnDefs : ReqInvScansRegistryService.columnDefs, + groupingNullLabel : $translate.instant('DEPOT.ALL_DEPOTS'), enableSorting : true, showColumnFooter : true, fastWatch : true, @@ -71,8 +72,6 @@ function RequiredInventoryScansRegistryController( vm.latestViewFilters = vm.filters.formatView(); } - // { key : 'period', value : 'allTime', displayValue : 'PERIODS.ALL_TIME', cacheable: false }, - // load the assets scans into the grid function load(filters) { if (vm.defaultDepot) { diff --git a/client/src/modules/shipment/create-shipment.html b/client/src/modules/shipment/create-shipment.html index 291044d53b..7d827da32d 100644 --- a/client/src/modules/shipment/create-shipment.html +++ b/client/src/modules/shipment/create-shipment.html @@ -247,6 +247,7 @@

STOCK.PRODUCT_NOT_AVAILABLE

FORM.LABELS.LABEL FORM.LABELS.TYPE + FORM.LABELS.WEIGHT @@ -256,6 +257,7 @@

STOCK.PRODUCT_NOT_AVAILABLE

{{cont.label}} {{cont.container_type}} + {{cont.weight}} FORM.LABELS.EDIT @@ -291,10 +293,8 @@

STOCK.PRODUCT_NOT_AVAILABLE

ui-grid-auto-resize ui-grid-resize-columns> + loading-state="CreateShipCtrl.stockForm.isLoading() || CreateShipCtrl.loading" + error-state="CreateShipCtrl.hasError"> diff --git a/client/src/modules/shipment/create-shipment.js b/client/src/modules/shipment/create-shipment.js index 84f7cae84a..bed983c47d 100644 --- a/client/src/modules/shipment/create-shipment.js +++ b/client/src/modules/shipment/create-shipment.js @@ -3,30 +3,30 @@ angular.module('bhima.controllers') // dependencies injections CreateShipmentController.$inject = [ - '$state', 'NotifyService', '$translate', - 'StockExitFormService', 'GridGroupingService', 'uiGridConstants', 'BarcodeService', 'ShipmentContainerService', - 'ShipmentService', 'DepotService', '$timeout', 'ShipmentModalService', - 'bhConstants', 'uuid', + '$state', 'ShipmentService', 'DepotService', 'StockExitFormService', + 'ShipmentModalService', 'ShipmentContainerService', 'uiGridConstants', + 'BarcodeService', 'NotifyService', 'bhConstants', 'uuid', '$translate', '$timeout', ]; function CreateShipmentController( - $state, Notify, $translate, - StockForm, Grouping, uiGridConstants, Barcode, Containers, - Shipment, Depot, $timeout, ShipmentModal, - bhConstants, Uuid, + $state, Shipment, Depot, StockForm, + ShipmentModal, Containers, uiGridConstants, + Barcode, Notify, bhConstants, Uuid, $translate, $timeout, ) { const { NOT_CREATED } = Containers; const vm = this; + vm.loading = false; + vm.depot = {}; - const existingShipmentUuid = $state.params.uuid; - vm.existingShipmentUuid = existingShipmentUuid; + const shipmentUuid = $state.params.uuid; + vm.existingShipmentUuid = shipmentUuid; + vm.isCreateState = $state.params.isCreateState; vm.shipment = {}; - vm.containers = []; vm.totalQuantity = 0; @@ -37,13 +37,6 @@ function CreateShipmentController( vm.stockForm.setExitTypePredefined(true); vm.stockForm.setExitType('depot'); - const gridFooterTemplate = ` -
- {{ grid.appScope.gridApi.core.getVisibleRows().length }} - TABLE.AGGREGATES.ROWS -
- `; - const gridColumns = [ { field : 'container_label', @@ -51,6 +44,7 @@ function CreateShipmentController( displayName : 'SHIPMENT.CONTAINER', headerCellFilter : 'translate', cellTemplate : 'modules/shipment/templates/container.tmpl.html', + sort : { direction : uiGridConstants.ASC, priority : 0 }, visible : false, }, { field : '_selected', @@ -58,31 +52,38 @@ function CreateShipmentController( headerCellTemplate : `
`, cellTemplate : 'modules/shipment/templates/checkbox.tmpl.html', + enableSorting : false, visible : false, }, { field : '_spacer', displayName : '', width : 10, + enableSorting : false, + visible : false, }, { field : 'status', width : 25, displayName : '', cellTemplate : 'modules/stock/exit/templates/status.tmpl.html', + enableSorting : false, }, { field : 'code', displayName : 'INVENTORY.CODE', headerCellFilter : 'translate', cellTemplate : 'modules/stock/exit/templates/code.tmpl.html', + enableSorting : false, }, { field : 'description', displayName : 'TABLE.COLUMNS.DESCRIPTION', headerCellFilter : 'translate', cellTemplate : 'modules/stock/exit/templates/description.tmpl.html', + enableSorting : false, }, { field : 'lot', displayName : 'TABLE.COLUMNS.LOT', headerCellFilter : 'translate', cellTemplate : 'modules/stock/exit/templates/lot.tmpl.html', + enableSorting : false, }, { field : 'quantity', width : 100, @@ -93,6 +94,7 @@ function CreateShipmentController( footerCellTemplate : `
${$translate.instant('SHIPMENT.TOTAL_QUANTITY')}: {{ grid.appScope.totalQuantity }}
`, + enableSorting : false, }, { field : 'unit_weight', width : 120, @@ -103,51 +105,56 @@ function CreateShipmentController( footerCellTemplate : `
${$translate.instant('SHIPMENT.TOTAL_WEIGHT')}: {{ grid.appScope.totalWeight }}
'`, + enableSorting : false, }, { field : 'unit_type', width : 75, displayName : 'TABLE.COLUMNS.UNIT', headerCellFilter : 'translate', cellTemplate : 'modules/stock/inventories/templates/unit.tmpl.html', + enableSorting : false, }, { field : '_quantity_available', width : 150, displayName : 'TABLE.COLUMNS.AVAILABLE', headerCellFilter : 'translate', cellTemplate : 'modules/stock/exit/templates/available.tmpl.html', + enableSorting : false, }, { displayName : '', field : 'actions', width : 25, cellTemplate : 'modules/stock/exit/templates/actions.tmpl.html', + enableSorting : false, }, ]; - const gridOptions = { - appScopeProvider : vm, - columnDefs : gridColumns, - enableColumnMenus : false, - enableSorting : false, - fastWatch : false, - flatEntityAccess : true, - gridFooterTemplate, + vm.gridOptions = { + appScopeProvider : vm, + columnDefs : gridColumns, + enableColumnMenus : false, + enableSorting : true, + fastWatch : true, + flatEntityAccess : true, + showColumnFooter : true, + showGridFooter : true, + gridFooterTemplate : 'modules/shipment/templates/shipmentGridFooter.tmpl.html', + data : vm.stockForm.store.data, onRegisterApi, - showColumnFooter : true, - showGridFooter : true, - - data : vm.stockForm.store.data, }; - vm.gridOptions = gridOptions; - - // THIS FAILS, not sure why! - // vm.grouping = new Grouping(vm.gridOptions, true, 'container_label', true, true); - - vm.gridApi = {}; - function onRegisterApi(gridApi) { vm.gridApi = gridApi; - checkVisibility(); + } + + function checkVisibility() { + // Hide the container columns if there are no containers. + const flag = vm.containers.length > 0; + ['container_label', '_selected', '_spacer'].forEach(colName => { + const column = vm.gridOptions.columnDefs.find(col => col.field === colName); + column.visible = flag; + }); + vm.gridApi.grid.refresh(); } vm.today = new Date(); @@ -162,18 +169,38 @@ function CreateShipmentController( }; function updateTotals() { + // Update the total quantity and weight of all shipment items + // (this does not include containers themselves) vm.totalQuantity = 0; vm.totalWeight = 0; vm.stockForm.store.data.forEach(row => { vm.totalQuantity += row.quantity || 0; vm.totalWeight += (row.quantity || 0) * (row.unit_weight || 0); }); + + // Add the weight of any containers + vm.containers.forEach(cntr => { + vm.totalWeight += cntr.weight; + }); + + // Also compute the total weight of each container + vm.containers.forEach(cntr => { + cntr.num_items = 0; + cntr.total_weight = cntr.weight; + vm.stockForm.store.data.forEach(row => { + if (row.container_label === cntr.label) { + cntr.num_items += row.quantity; + cntr.total_weight += (row.quantity || 0) * (row.unit_weight || 0); + } + }); + }); } vm.validateItems = () => { updateTotals(); vm.stockForm.validate(true); vm.messages = vm.stockForm.messages(); + checkVisibility(); }; vm.configureItem = function configureItem(row, lot) { @@ -186,6 +213,7 @@ function CreateShipmentController( if (vm.containers.length) { const last = vm.containers[vm.containers.length - 1]; lot.container_uuid = last.uuid; + lot.container_label = last.label; } lot.unit_type = lot.unit; @@ -210,16 +238,6 @@ function CreateShipmentController( vm.shipment.anticipated_delivery_date = date; }; - function checkVisibility() { - // Hide the container columns if there are no containers. - const flag = vm.containers.length > 0; - ['container_label', '_selected', '_spacer'].forEach(colName => { - const col = vm.gridOptions.columnDefs.find(c => c.field === colName); - col.visible = flag; - }); - vm.gridApi.grid.refresh(); - } - vm.newContainer = function newContainer() { ShipmentModal.openEditContainerModal({ action : 'create' }) .then((result) => { @@ -229,6 +247,9 @@ function CreateShipmentController( // database. So we need to add a temporary uuid for internal use here. result.uuid = Uuid(); result[NOT_CREATED] = true; + if (!angular.isDefined(result.weight)) { + result.weight = 0.0; + } vm.containers.push(result); // NOTE: We are saving the data for containers but not creating @@ -246,6 +267,7 @@ function CreateShipmentController( }) .catch(Notify.handleError) .finally(() => { + updateTotals(); checkVisibility(); }); @@ -261,10 +283,14 @@ function CreateShipmentController( // AND it updates the existing containers in memory without reloading them) const oldCont = vm.containers.find(cont => cont.uuid === container.uuid); oldCont.label = result.label; + oldCont.weight = result.weight; oldCont.container_type_id = result.container_type_id; oldCont.container_type = result.container_type; }) - .catch(Notify.handleError); + .catch(Notify.handleError) + .finally(() => { + updateTotals(); + }); }; vm.deleteContainer = function deleteContainer(container) { @@ -356,7 +382,7 @@ function CreateShipmentController( return vm.stockForm.setDepot(depot) .then(() => { - // trick an exit type which is required + // ??? trick an exit type which is required vm.stockForm.setExitType('loss'); vm.stockForm.setLossDistribution(); @@ -375,7 +401,7 @@ function CreateShipmentController( } function getOverview(uuid) { - return ShipmentModal.shipmentOverviewModal(uuid); + return ShipmentModal.shipmentDocumentModal(uuid); } function reset(form) { @@ -385,14 +411,12 @@ function CreateShipmentController( } function startup() { - vm.$loading = true; + vm.loading = true; vm.hasError = false; - vm.stockForm.setup(); + vm.stockForm.setup(); vm.stockForm.setExitType('depot'); - vm.validateItems(); - // load the shipment for update loadShipment(); } @@ -444,6 +468,11 @@ function CreateShipmentController( function submit(form) { + // This is a work-round to make sure that updating shipments works + // (The exit_type is getting lost somehow, possibly because we are + // using StockExitForm in an unconventional way). + vm.stockForm.details.exit_type = 'depot'; + if (form.$invalid) { vm.validateItems(); @@ -486,7 +515,7 @@ function CreateShipmentController( return null; } - vm.$loading = true; + vm.loading = true; vm.shipment = cleanShipment(vm.shipment); @@ -500,11 +529,11 @@ function CreateShipmentController( const promise = !!(vm.isCreateState) ? Shipment.create(vm.shipment) - : Shipment.update(existingShipmentUuid, vm.shipment); + : Shipment.update(shipmentUuid, vm.shipment); return promise .then((res) => { - vm.shipment.uuid = vm.isCreateState ? res.uuid : existingShipmentUuid; + vm.shipment.uuid = vm.isCreateState ? res.uuid : shipmentUuid; reset(form); return true; }) @@ -528,7 +557,7 @@ function CreateShipmentController( }) .catch(Notify.handleError) .finally(() => { - vm.$loading = false; + vm.loading = false; }); } @@ -553,18 +582,20 @@ function CreateShipmentController( row.unit_type = lot.unit_type; row.container_uuid = lot.container_uuid; row.container_label = lot.container_label; + row.item_uuid = lot.item_uuid; row._selected = 0; }); } // this function function loadShipment() { - if (!existingShipmentUuid) { - vm.$loading = false; + + if (!shipmentUuid) { + vm.loading = false; return; } - Shipment.readAll(existingShipmentUuid) + Shipment.readAll(shipmentUuid) .then(shipment => { vm.shipment = shipment; vm.shipment.anticipated_delivery_date = new Date(vm.shipment.anticipated_delivery_date); @@ -583,23 +614,23 @@ function CreateShipmentController( }) .then(destDepot => { delete vm.messages; - vm.stockForm.setExitType('depot'); vm.stockForm.setDepotDistribution(destDepot); vm.stockForm.setLotsFromShipmentList(vm.shipment.lots, 'lot_uuid'); updateLotsData(vm.shipment.lots); + vm.validateItems(); }) .catch(Notify.handleError) .finally(() => { - vm.$loading = false; + vm.loading = false; }); } function fetchAllocatedAssets() { - const isEdit = !vm.isCreateState && existingShipmentUuid; + const isEdit = !vm.isCreateState && shipmentUuid; return Shipment.getAllocatedAssets({ origin_depot_uuid : vm.depot.uuid, currently_at_depot : true, - except_current_shipment : isEdit ? existingShipmentUuid : undefined, + except_current_shipment : isEdit ? shipmentUuid : undefined, }); } diff --git a/client/src/modules/shipment/modals/edit-container.modal.html b/client/src/modules/shipment/modals/edit-container.modal.html index 68a197e08b..4164275a63 100644 --- a/client/src/modules/shipment/modals/edit-container.modal.html +++ b/client/src/modules/shipment/modals/edit-container.modal.html @@ -22,19 +22,26 @@
- + {{$select.selected.text}} - + -
+
+
+ + +
diff --git a/client/src/modules/shipment/modals/edit-container.modal.js b/client/src/modules/shipment/modals/edit-container.modal.js index e61cf19e6c..e0a52ccce5 100644 --- a/client/src/modules/shipment/modals/edit-container.modal.js +++ b/client/src/modules/shipment/modals/edit-container.modal.js @@ -51,6 +51,7 @@ function ContainerEditModalController(Data, Containers, Notify, Instance) { // The container already exists, so update it in the database immediately const updates = { // These are the only fields that we can update label : vm.container.label, + weight : vm.container.weight, container_type_id : vm.container.container_type_id, }; Containers.update(vm.container.uuid, updates) diff --git a/client/src/modules/shipment/modals/overview.modal.html b/client/src/modules/shipment/modals/shipment-document.modal.html similarity index 100% rename from client/src/modules/shipment/modals/overview.modal.html rename to client/src/modules/shipment/modals/shipment-document.modal.html diff --git a/client/src/modules/shipment/modals/overview.modal.js b/client/src/modules/shipment/modals/shipment-document.modal.js similarity index 77% rename from client/src/modules/shipment/modals/overview.modal.js rename to client/src/modules/shipment/modals/shipment-document.modal.js index 9200b24c59..6f430bbe30 100644 --- a/client/src/modules/shipment/modals/overview.modal.js +++ b/client/src/modules/shipment/modals/shipment-document.modal.js @@ -1,11 +1,11 @@ angular.module('bhima.controllers') - .controller('ShipmentOverviewModalController', ShipmentOverviewModalController); + .controller('ShipmentDocumentModalController', ShipmentDocumentModalController); -ShipmentOverviewModalController.$inject = [ +ShipmentDocumentModalController.$inject = [ '$window', 'params', 'ShipmentService', 'NotifyService', '$uibModalInstance', ]; -function ShipmentOverviewModalController($window, params, Shipments, Notify, Instance) { +function ShipmentDocumentModalController($window, params, Shipments, Notify, Instance) { const vm = this; const identifier = params.uuid; diff --git a/client/src/modules/shipment/shipment.js b/client/src/modules/shipment/shipment.js index 579bd3b49e..bcede3e74a 100644 --- a/client/src/modules/shipment/shipment.js +++ b/client/src/modules/shipment/shipment.js @@ -2,15 +2,15 @@ angular.module('bhima.controllers') .controller('ShipmentRegistryController', ShipmentRegistryController); ShipmentRegistryController.$inject = [ - '$state', 'ShipmentService', 'ShipmentFilter', 'ShipmentModalService', - 'ModalService', 'NotifyService', 'uiGridConstants', - 'GridStateService', 'GridColumnService', 'bhConstants', + '$state', 'ShipmentService', 'ShipmentFilter', 'ShipmentModalService', 'ModalService', + 'uiGridConstants', 'GridStateService', 'GridColumnService', + 'NotifyService', 'bhConstants', ]; function ShipmentRegistryController( - $state, Shipments, ShipmentFilter, ShipmentModal, - Modal, Notify, GridConstants, - GridState, Columns, Constants, + $state, Shipments, ShipmentFilter, ShipmentModal, Modal, + GridConstants, GridState, Columns, + Notify, Constants, ) { const vm = this; const cacheKey = 'shipment-grid'; @@ -30,7 +30,8 @@ function ShipmentRegistryController( vm.toggleFilter = toggleFilter; vm.onRemoveFilter = onRemoveFilter; vm.search = search; - vm.shipmentOverview = shipmentOverview; + vm.shipmentDocument = shipmentDocument; + vm.shipmentManifest = shipmentManifest; vm.getShipmentBarcode = getShipmentBarcode; vm.gotoStockEntry = gotoStockEntry; vm.gotoStockExit = gotoStockExit; @@ -182,12 +183,16 @@ function ShipmentRegistryController( $state.reload(); } - function shipmentOverview(uuid) { - return ShipmentModal.openShipmentOverview(uuid); + function shipmentDocument(uuid) { + return ShipmentModal.openShipmentDocument(uuid); + } + + function shipmentManifest(uuid) { + return ShipmentModal.openShipmentManifest(uuid); } function getOverview(uuid) { - return ShipmentModal.shipmentOverviewModal(uuid); + return ShipmentModal.shipmentDocumentModal(uuid); } function setReady(uuid) { diff --git a/client/src/modules/shipment/shipment.modal.service.js b/client/src/modules/shipment/shipment.modal.service.js index ab4bd965ca..352a1b60cb 100644 --- a/client/src/modules/shipment/shipment.modal.service.js +++ b/client/src/modules/shipment/shipment.modal.service.js @@ -21,24 +21,25 @@ function ShipmentModalService(Modal, Receipts) { animation : false, }; - service.shipmentOverviewModal = shipmentOverviewModal; + service.shipmentDocumentModal = shipmentDocumentModal; service.setReadyForShipmentModal = setReadyForShipmentModal; service.updateTrackingLogModal = updateTrackingLogModal; service.setShipmentDeliveredModal = setShipmentDeliveredModal; service.setShipmentCompletedModal = setShipmentCompletedModal; service.openSearchShipment = openSearchShipment; - service.openShipmentOverview = openShipmentOverview; + service.openShipmentDocument = openShipmentDocument; + service.openShipmentManifest = openShipmentManifest; service.openShipmentBarcode = openShipmentBarcode; service.openEditContainerModal = openEditContainerModal; // modal on the client callable from anywhere - function shipmentOverviewModal(uuid) { + function shipmentDocumentModal(uuid) { Modal.open({ size : 'lg', - templateUrl : 'modules/shipment/modals/overview.modal.html', - controller : 'ShipmentOverviewModalController as $ctrl', + templateUrl : 'modules/shipment/modals/shipment-document.modal.html', + controller : 'ShipmentDocumentModalController as $ctrl', resolve : { params : () => ({ uuid }) }, }).result.catch(angular.noop); } @@ -89,9 +90,15 @@ function ShipmentModalService(Modal, Receipts) { return instance.result; } - function openShipmentOverview(documentUuid, notifyCreated) { - const opts = { title : 'SHIPMENT.OVERVIEW', notifyCreated, renderer : Receipts.renderer }; - const promise = Receipts.shipmentOverview(documentUuid, { renderer : opts.renderer }); + function openShipmentDocument(documentUuid, notifyCreated) { + const opts = { title : 'SHIPMENT.SHIPMENT_DOCUMENT', notifyCreated, renderer : Receipts.renderer }; + const promise = Receipts.shipmentDocument(documentUuid, { renderer : opts.renderer }); + return ReceiptFactory(promise, opts); + } + + function openShipmentManifest(documentUuid, notifyCreated) { + const opts = { title : 'SHIPMENT.SHIPMENT_MANIFEST', notifyCreated, renderer : Receipts.renderer }; + const promise = Receipts.shipmentManifest(documentUuid, { renderer : opts.renderer }); return ReceiptFactory(promise, opts); } diff --git a/client/src/modules/shipment/templates/action.tmpl.html b/client/src/modules/shipment/templates/action.tmpl.html index 57adcf872f..993b72263a 100644 --- a/client/src/modules/shipment/templates/action.tmpl.html +++ b/client/src/modules/shipment/templates/action.tmpl.html @@ -11,18 +11,16 @@
  • - - FORM.LABELS.DOCUMENT + + SHIPMENT.DOCUMENT
  • -
  • - - - SHIPMENT.TRACKING_LOG - + + SHIPMENT.MANIFEST
  • +
  • @@ -30,7 +28,6 @@
  • -
  • @@ -38,6 +35,15 @@
  • +
  • +
  • + + + SHIPMENT.TRACKING_LOG + + +
  • +
  • diff --git a/client/src/modules/shipment/templates/shipmentGridFooter.tmpl.html b/client/src/modules/shipment/templates/shipmentGridFooter.tmpl.html new file mode 100644 index 0000000000..73d6699f8b --- /dev/null +++ b/client/src/modules/shipment/templates/shipmentGridFooter.tmpl.html @@ -0,0 +1,29 @@ + +
    +

    {{ grid.appScope.gridApi.core.getVisibleRows().length }} STOCK.ITEMS

    +
    +
     
    +

    SHIPMENT.CONTAINERS

    + + + + + + + + + + + + + + + + + + + +
    FORM.LABELS.LABELFORM.LABELS.TYPESHIPMENT.NUM_ITEMSSHIPMENT.CONTAINER_WEIGHT_EMPTYSHIPMENT.CONTAINER_WEIGHT_PACKED
    {{cont.label}}{{cont.container_type}}{{cont.num_items}}{{cont.weight}}{{cont.total_weight}}
    +
    +
    + diff --git a/client/src/modules/stock/StockExitForm.service.js b/client/src/modules/stock/StockExitForm.service.js index 69451bd7a2..9bea077ae4 100644 --- a/client/src/modules/stock/StockExitForm.service.js +++ b/client/src/modules/stock/StockExitForm.service.js @@ -109,7 +109,9 @@ function StockExitFormService( }; // show the informational message that we need to select an exit type. - this._toggleInfoMessage(true, 'info', INFO_NO_EXIT_TYPE, this.details); + if (!this.exitTypePredefined) { + this._toggleInfoMessage(true, 'info', INFO_NO_EXIT_TYPE, this.details); + } }; /** diff --git a/client/src/modules/stock/inventories/registry.js b/client/src/modules/stock/inventories/registry.js index 905369f2f4..b705c55efb 100644 --- a/client/src/modules/stock/inventories/registry.js +++ b/client/src/modules/stock/inventories/registry.js @@ -2,11 +2,9 @@ angular.module('bhima.controllers') .controller('StockInventoriesController', StockInventoriesController); StockInventoriesController.$inject = [ - 'StockService', 'NotifyService', - 'uiGridConstants', 'StockModalService', 'LanguageService', 'SessionService', - 'GridGroupingService', 'bhConstants', 'GridStateService', - '$state', 'GridColumnService', '$httpParamSerializer', 'BarcodeService', - '$translate', + '$state', 'StockService', 'StockModalService', 'LanguageService', 'SessionService', + 'uiGridConstants', 'GridGroupingService', 'GridStateService', 'GridColumnService', + 'NotifyService', '$httpParamSerializer', 'BarcodeService', '$translate', 'bhConstants', ]; /** @@ -14,9 +12,9 @@ StockInventoriesController.$inject = [ * This module is a registry page for stock inventories */ function StockInventoriesController( - Stock, Notify, uiGridConstants, Modal, Languages, - Session, Grouping, bhConstants, GridState, $state, Columns, - $httpParamSerializer, Barcode, $translate, + $state, Stock, StockModal, Languages, Session, + uiGridConstants, Grouping, GridState, Columns, + Notify, $httpParamSerializer, Barcode, $translate, bhConstants, ) { const vm = this; const cacheKey = 'stock-inventory-grid'; @@ -300,7 +298,7 @@ function StockInventoriesController( function search() { const filtersSnapshot = stockInventoryFilters.formatHTTP(); - Modal.openSearchInventories(filtersSnapshot) + StockModal.openSearchInventories(filtersSnapshot) .then((changes) => { if (!changes) { return; } stockInventoryFilters.replaceFilters(changes); @@ -355,13 +353,13 @@ function StockInventoriesController( vm.viewAMCCalculations = viewAMCCalculations; function viewAMCCalculations(item) { - Modal.openAMCCalculationModal(item) + StockModal.openAMCCalculationModal(item) .catch(angular.noop); } // lot schedule modal vm.openLotScheduleModal = (uuid, inventoryUuid, depotUuid) => { - Modal.openLotScheduleModal({ uuid, inventoryUuid, depotUuid }) + StockModal.openLotScheduleModal({ uuid, inventoryUuid, depotUuid }) .catch(angular.noop); }; diff --git a/docs/en/stock-management/movement.shipments.md b/docs/en/stock-management/movement.shipments.md index 6d0310ffa7..55b6c11a46 100644 --- a/docs/en/stock-management/movement.shipments.md +++ b/docs/en/stock-management/movement.shipments.md @@ -92,7 +92,7 @@ Here is a brief recap of the actions available at this point: To complete this step in the example, select the "Ready to Ship" option and confirm the action in the modal dialog.
    -**NOTE**: An important thing to note about the action menu for shipments is that it will only display actions that can be done based on the current status of the shipment. This makes it straightforward to process shipments since the action menu essentially guides the user through the normal process of dealing with shipments. +NOTE: An important thing about the action menu for shipments is that it will only display actions that can be done based on the current status of the shipment. This makes it straightforward to process shipments since the action menu essentially guides the user through the normal process of dealing with shipments.
    ### Stock Exit at the Sending Depot @@ -138,11 +138,11 @@ shipments to support this. For example: Lots" option for the desired container in the container operations menu. When the **\[Containers\]** menu is opened, it appears like this: -Shipment container operation +Shipment container operation
    The options for each container are: - **Edit** - Edit the container - **Assign lots** - If this command is executed, all shipment items (lots) - whos "Multiple lot select* checkbox are checked will be reassigned to this + whose "Multiple lot select* checkbox are checked will be reassigned to this container. - **Delete** - Deletes the container. All items in that container will be not be assigned a new container. diff --git a/server/config/routes.js b/server/config/routes.js index b5f4aaf647..d6c186d471 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -1145,8 +1145,9 @@ exports.configure = function configure(app) { app.put('/shipments/:uuid/shipment-completed', shipment.setShipmentCompleted); app.delete('/shipments/:uuid', shipment.deleteShipment); app.post('/shipments', shipment.create); + app.get('/reports/shipments', shipment.getReport); - app.get('/reports/shipments/:uuid/overview', shipment.getOverview); + app.get('/reports/shipments/:uuid/document', shipment.getDocument); app.get('/reports/shipments/:uuid/barcode', shipment.getBarcode); app.get('/shipment_container_types', shipmentContainer.listTypes); diff --git a/server/controllers/asset_management/shipment/index.js b/server/controllers/asset_management/shipment/index.js index 1a02591dd6..358a652341 100644 --- a/server/controllers/asset_management/shipment/index.js +++ b/server/controllers/asset_management/shipment/index.js @@ -1,6 +1,6 @@ const shipment = require('./shipment'); const shipmentReports = require('./reports/shipments'); -const shipmentOverviewReceipt = require('./reports/shipment-overview'); +const shipmentDocumentReceipt = require('./reports/shipment-document'); const shipmentBarcodeReceipt = require('./reports/shipment-barcode'); module.exports = { @@ -17,7 +17,7 @@ module.exports = { setShipmentCompleted : shipment.setShipmentCompleted, listInTransitInventories : shipment.listInTransitInventories, getReport : shipmentReports.getReport, - getOverview : shipmentOverviewReceipt.getShipmentOverview, + getDocument : shipmentDocumentReceipt.getShipmentDocument, getBarcode : shipmentBarcodeReceipt.getBarcode, writeStockExitShipment : shipment.writeStockExitShipment, writeStockEntryShipment : shipment.writeStockEntryShipment, diff --git a/server/controllers/asset_management/shipment/reports/common.js b/server/controllers/asset_management/shipment/reports/common.js index 090215c11e..6a7e5e2985 100644 --- a/server/controllers/asset_management/shipment/reports/common.js +++ b/server/controllers/asset_management/shipment/reports/common.js @@ -5,12 +5,14 @@ const NotFound = require('../../../../lib/errors/NotFound'); const { formatFilters } = require('../../../finance/reports/shared'); const barcode = require('../../../../lib/barcode'); const identifiers = require('../../../../config/identifiers'); -const shipment = require('../shipment'); +const Shipment = require('../shipment'); +const ShipmentContainer = require('../shipment_containers'); // handlebars templates const BASE_URL = './server/controllers/asset_management/shipment'; const SHIPMENTS_REPORT_TEMPLATE = `${BASE_URL}/reports/shipments.handlebars`; -const SHIPMENT_OVERVIEW_TEMPLATE = `${BASE_URL}/reports/shipment-overview.handlebars`; +const SHIPMENT_DOCUMENT_TEMPLATE = `${BASE_URL}/reports/shipment-document.handlebars`; +const SHIPMENT_MANIFEST_TEMPLATE = `${BASE_URL}/reports/shipment-manifest.handlebars`; const SHIPMENT_BARCODE_TEMPLATE = `${BASE_URL}/reports/shipment-barcode.handlebars`; module.exports = { @@ -21,10 +23,12 @@ module.exports = { formatFilters, barcode, identifiers, - shipment, + Shipment, + ShipmentContainer, // export handlebars templates SHIPMENTS_REPORT_TEMPLATE, - SHIPMENT_OVERVIEW_TEMPLATE, + SHIPMENT_DOCUMENT_TEMPLATE, + SHIPMENT_MANIFEST_TEMPLATE, SHIPMENT_BARCODE_TEMPLATE, }; diff --git a/server/controllers/asset_management/shipment/reports/shipment-overview.handlebars b/server/controllers/asset_management/shipment/reports/shipment-document.handlebars similarity index 69% rename from server/controllers/asset_management/shipment/reports/shipment-overview.handlebars rename to server/controllers/asset_management/shipment/reports/shipment-document.handlebars index d944969a47..9d6aacb8be 100644 --- a/server/controllers/asset_management/shipment/reports/shipment-overview.handlebars +++ b/server/controllers/asset_management/shipment/reports/shipment-document.handlebars @@ -1,9 +1,13 @@ {{> head }} @@ -88,12 +92,12 @@ {{/if}} -
    +
    {{translate 'SHIPMENT.TRANSPORT_MODE'}}: {{shipment.transport_mode}}
    -

    {{translate 'SHIPMENT.RECEIVER'}}: {{shipment.receiver}}

    + {{translate 'SHIPMENT.RECEIVER'}}: {{shipment.receiver}}

    @@ -102,7 +106,10 @@
    -

    {{translate 'SHIPMENT.PACKING_LIST'}}

    +

    {{translate 'SHIPMENT.PACKING_LIST'}} + {{#if shipment.hasContainers}}({{shipment.numContainers}} {{translate shipment.multipleContainerLabel}}) + {{/if}} +

    @@ -116,18 +123,32 @@ - {{#each records}} - - - - - - - - - - {{else}} - {{> emptyTable columns=7}} + {{#each contents}} + {{#if ../shipment.hasContainers}} + + + + {{/if}} + {{#each items}} + + + + + + + + + + {{else}} + {{> emptyTable columns=7}} + {{/each}} {{/each}} @@ -142,7 +163,12 @@ - + @@ -154,24 +180,6 @@ -
    -
    -

    {{translate 'SHIPMENT.AUTHORIZED'}}:

    -
    -
    -

    {{translate 'SHIPMENT.TRANSPORTER'}}:

    -
    -
    - -
    -
    -

    {{translate 'SHIPMENT.EXECUTED_BY'}}:

    -
    -
    -

    {{translate 'SHIPMENT.DUELY_RECEIVED_BY'}}:

    -
    {{translate 'SHIPMENT.NAME_FUNCTION_SIGNATURE'}}
    -
    -
    diff --git a/server/controllers/asset_management/shipment/reports/shipment-document.js b/server/controllers/asset_management/shipment/reports/shipment-document.js new file mode 100644 index 0000000000..4f53229ccd --- /dev/null +++ b/server/controllers/asset_management/shipment/reports/shipment-document.js @@ -0,0 +1,121 @@ +const _ = require('lodash'); + +const { + ReportManager, + db, barcode, identifiers, + Shipment, ShipmentContainer, + SHIPMENT_DOCUMENT_TEMPLATE, SHIPMENT_MANIFEST_TEMPLATE, +} = require('./common'); + +function sortItems(items) { + items.sort((a, b) => { + const atext = a.inventory_label.toLowerCase(); + const btext = b.inventory_label.toLowerCase(); + return atext.localeCompare(btext); + }); +} + +async function getShipmentDocument(req, res, next) { + const { uuid } = req.params; + const manifest = req.query.manifest || false; + + const template = manifest ? SHIPMENT_MANIFEST_TEMPLATE : SHIPMENT_DOCUMENT_TEMPLATE; + + const options = _.extend(req.query, { + filename : manifest ? 'SHIPMENT.SHIPMENT_MANIFEST' : 'SHIPMENT.TITLE', + orientation : 'portrait', + }); + + try { + const report = new ReportManager(template, req.session, options); + const shipment = await Shipment.lookupSingle(uuid); + const shipmentItems = await Shipment.getPackingList(uuid); + const containers = await ShipmentContainer.containersForShipment(uuid); + + const depotSql = ` + SELECT + BUID(d.uuid) as uuid, d.text, d.description, d.is_warehouse, + BUID(d.location_uuid) AS location_uuid, + v.name as village_name, s.name as sector_name, + p.name as province_name, c.name as country_name + FROM depot AS d + LEFT JOIN village v ON v.uuid = d.location_uuid + LEFT JOIN sector s ON s.uuid = v.sector_uuid + LEFT JOIN province p ON p.uuid = s.province_uuid + LEFT JOIN country c ON c.uuid = p.country_uuid + WHERE d.uuid = ?; + `; + + // Get the depot info + const [originDepot] = await db.exec(depotSql, [db.bid(shipment.origin_depot_uuid)]); + const odes = originDepot.description; + if (typeof odes === 'string') { + originDepot.description = odes.split('\n'); + } + shipment.origin_depot = originDepot; + + const [destinationDepot] = await db.exec(depotSql, [db.bid(shipment.destination_depot_uuid)]); + const ddes = destinationDepot.description; + if (typeof ddes === 'string') { + destinationDepot.description = ddes.split('\n'); + } + shipment.destination_depot = destinationDepot; + + // Compute totals + shipmentItems.forEach(row => { + row.cost = row.quantity_sent * row.unit_price; + row.weight = row.quantity_sent * row.unit_weight; + }); + + shipment.barcode = barcode.generate(identifiers.SHIPMENT.key, shipment.uuid); + + shipment.totalNumItems = shipmentItems.length; + + shipment.totalCost = shipmentItems.reduce((agg, row) => agg + row.cost, 0); + shipment.totalQuantity = shipmentItems.reduce((agg, row) => agg + row.quantity_sent, 0); + shipment.totalWeight = shipmentItems.reduce((agg, row) => agg + row.weight, 0); + + shipment.hasContainers = containers.length > 0; + shipment.numContainers = containers.length; + shipment.multipleContainers = containers.length > 1; + shipment.multipleContainerLabel = shipment.multipleContainers ? 'SHIPMENT.CONTAINERS' : 'SHIPMENT.CONTAINER'; + + // Construct the contents list for display + let contents = []; + if (shipment.hasContainers) { + const data = _.groupBy(shipmentItems, 'container_label'); + containers.forEach(cntr => { + const containerItems = data[cntr.label]; + sortItems(containerItems); + contents.push({ + containerName : cntr.label, + containerEmptyWeight : cntr.weight, + containerType : `SHIPMENT.CONTAINER_TYPES.${cntr.container_type}`, + containerWeight : cntr.weight + containerItems.reduce((agg, row) => agg + row.weight, 0), + containerValue : containerItems.reduce((agg, row) => agg + row.cost, 0), + items : containerItems, + }); + shipment.totalWeight += cntr.weight; + }); + } else { + sortItems(shipmentItems); + contents = [{ + containerName : null, + items : shipmentItems, + }]; + } + + const data = { + shipment, + contents, + date : new Date(), + }; + + const result = await report.render(data); + res.set(result.headers).send(result.report); + } catch (e) { + next(e); + } +} + +exports.getShipmentDocument = getShipmentDocument; diff --git a/server/controllers/asset_management/shipment/reports/shipment-manifest.handlebars b/server/controllers/asset_management/shipment/reports/shipment-manifest.handlebars new file mode 100644 index 0000000000..435afc1417 --- /dev/null +++ b/server/controllers/asset_management/shipment/reports/shipment-manifest.handlebars @@ -0,0 +1,223 @@ +{{> head }} + + + + + +
    + {{> enterpriseDetails }} + +
    + +

    + {{translate 'SHIPMENT.SHIPMENT_MANIFEST'}}: {{shipment.name}} - {{shipment.reference}} +

    + +

    + {{translate 'SHIPMENT.SHIPMENT_STATUS'}}: {{translate shipment.status}} +

    + + {{#if metadata.enterprise.settings.enable_barcodes}} +

    + {{> barcode value=barcode}} +

    + + {{/if}} + + +
    + {{translate 'FORM.LABELS.DATE'}}: {{date shipment.date}} +
    + +
    + +
    + + + +
    +
    +

    {{translate 'STOCK.ORIGIN'}}

    +

    {{shipment.origin_depot.text}}

    + {{#if shipment.origin_depot.description}} +
    + {{#each shipment.origin_depot.description}} +
    {{this}} + {{/each}} +
    + {{/if}} + {{#if shipment.origin_depot.location_uuid}} + {{#with shipment.origin_depot}} + {{village_name}} / {{sector_name}} / {{province_name}}
    + {{country_name}} + {{/with}} + {{/if}} +
    +
    +
    +

    {{translate 'STOCK.DESTINATION'}}

    +

    {{shipment.destination_depot.text}}

    +
    + {{translate 'SHIPMENT.ANTICIPATED_DELIVERY_DATE'}}: {{date shipment.anticipated_delivery_date}} +
    + {{#if shipment.destination_depot.description}} +
    + {{#each shipment.destination_depot.description}} +
    {{this}} + {{/each}} +
    + {{/if}} + {{#if shipment.destination_depot.location_uuid}} + {{#with shipment.destination_depot}} + {{village_name}} / {{sector_name}} / {{province_name}}
    + {{country_name}} + {{/with}} + {{/if}} +
    +
    + +{{#if shipment.description}} +
    +
    + {{shipment.description}} +
    +
    +{{/if}} + +
    +
    + {{translate 'SHIPMENT.TRANSPORT_MODE'}}: {{shipment.transport_mode}} +
    +
    +

    {{translate 'SHIPMENT.RECEIVER'}}: {{shipment.receiver}}

    +
    +
    +
    + +
    +
    + + +

    {{translate 'SHIPMENT.PACKING_LIST'}} + {{#if shipment.hasContainers}}({{shipment.numContainers}} {{translate shipment.multipleContainerLabel}}) + {{/if}} +

    +
    {{inventory_label}}{{lot_label}}{{unit_type}}{{quantity_sent}}{{unit_weight}}{{currency unit_price ../metadata.enterprise.currency_id}}{{currency cost ../metadata.enterprise.currency_id}}
    + + {{translate 'SHIPMENT.CONTAINER'}}: {{containerName}} ({{translate containerType }}) + , + {{translate 'SHIPMENT.TOTAL_WEIGHT'}}: {{containerWeight}} + ({{translate 'SHIPMENT.EMPTY_WEIGHT'}}: {{containerEmptyWeight}}) +
    + {{inventory_label}} ({{inventory_code}}){{lot_label}}{{unit_type}}{{quantity_sent}}{{unit_weight}}{{currency unit_price ../../metadata.enterprise.currency_id}}{{currency cost ../../metadata.enterprise.currency_id}}
    {{translate 'SHIPMENT.TOTAL_COST'}}
    {{records.length}} {{translate 'STOCK.ITEMS'}} + {{#if shipment.hasContainers}} + {{shipment.numContainers}} {{translate shipment.multipleContainerLabel}}, + {{/if}} + {{shipment.totalNumItems}} {{translate 'SHIPMENT.ITEMS'}} + {{shipment.totalQuantity}} {{shipment.totalWeight}}
    + + + {{#if shipment.hasContainers}} + + + + + + {{else}} + + + + + + + + {{/if}} + + + + {{#each contents}} + {{#if ../shipment.hasContainers}} + + + + + + + + {{else}} + {{#each items}} + + + + + + + + + + {{else}} + {{> emptyTable columns=7}} + {{/each}} + {{/if}} + {{/each}} + + + + + + + + {{#if shipment.hasContainers}} + + + + {{else}} + + + + + {{/if}} + + + + {{#if shipment.hasContainers}} + + + + {{else}} + + + + + {{/if}} + + +
    {{translate 'SHIPMENT.CONTAINER'}}{{translate 'SHIPMENT.CONTAINER_TYPE'}}{{translate 'SHIPMENT.CONTAINER_WEIGHT_EMPTY'}}{{translate 'SHIPMENT.CONTAINER_WEIGHT_PACKED'}}{{translate 'SHIPMENT.CONTAINER_VALUE'}}{{translate 'STOCK.INVENTORY'}}{{translate 'STOCK.LOT'}}{{translate 'TABLE.COLUMNS.UNIT'}}{{translate 'STOCK.QUANTITY'}}{{translate 'TABLE.COLUMNS.UNIT_WEIGHT'}}{{translate 'STOCK.UNIT_COST'}}{{translate 'STOCK.COST'}}
    {{containerName}}{{translate containerType }}{{containerEmptyWeight}}{{containerWeight}}{{currency containerValue metadata.enterprise.currency_id}}
    + {{inventory_label}} ({{inventory_code}}){{lot_label}}{{unit_type}}{{quantity_sent}}{{unit_weight}}{{currency unit_price ../../metadata.enterprise.currency_id}}{{currency cost ../../metadata.enterprise.currency_id}}
    {{translate 'SHIPMENT.TOTAL_WEIGHT'}}{{translate 'SHIPMENT.TOTAL_VALUE'}}{{translate 'SHIPMENT.TOTAL_QUANTITY'}}{{translate 'SHIPMENT.TOTAL_WEIGHT'}}{{translate 'SHIPMENT.TOTAL_COST'}}
    + {{#if shipment.hasContainers}} + {{shipment.numContainers}} {{translate shipment.multipleContainerLabel}} + {{else}} + {{shipment.totalNumItems}} {{translate 'SHIPMENT.ITEMS'}} + {{/if}} + {{shipment.totalWeight}}{{currency shipment.totalCost metadata.enterprise.currency_id}}{{shipment.totalQuantity}}{{shipment.totalWeight}}{{currency shipment.totalCost metadata.enterprise.currency_id}}
    + +
    +
    + +
    +
    +

    {{translate 'SHIPMENT.AUTHORIZED'}}:

    +
    +
    +

    {{translate 'SHIPMENT.TRANSPORTER'}}:

    +
    +
    + +
    +
    +

    {{translate 'SHIPMENT.EXECUTED_BY'}}:

    +
    +
    +

    {{translate 'SHIPMENT.DUELY_RECEIVED_BY'}}:

    +
    {{translate 'SHIPMENT.NAME_FUNCTION_SIGNATURE'}}
    +
    +
    + + + diff --git a/server/controllers/asset_management/shipment/reports/shipment-overview.js b/server/controllers/asset_management/shipment/reports/shipment-overview.js deleted file mode 100644 index 53b680954f..0000000000 --- a/server/controllers/asset_management/shipment/reports/shipment-overview.js +++ /dev/null @@ -1,71 +0,0 @@ -const { - ReportManager, - db, barcode, identifiers, - shipment, SHIPMENT_OVERVIEW_TEMPLATE, -} = require('./common'); - -async function getShipmentOverview(req, res, next) { - const { uuid } = req.params; - - try { - const report = new ReportManager(SHIPMENT_OVERVIEW_TEMPLATE, req.session, { - filename : 'SHIPMENT.TITLE', - orientation : 'portrait', - }); - const shipmentDetails = await shipment.lookupSingle(uuid); - const records = await shipment.getPackingList(uuid); - - const depotSql = ` - SELECT - BUID(d.uuid) as uuid, d.text, d.description, d.is_warehouse, - BUID(d.location_uuid) AS location_uuid, - v.name as village_name, s.name as sector_name, - p.name as province_name, c.name as country_name - FROM depot AS d - LEFT JOIN village v ON v.uuid = d.location_uuid - LEFT JOIN sector s ON s.uuid = v.sector_uuid - LEFT JOIN province p ON p.uuid = s.province_uuid - LEFT JOIN country c ON c.uuid = p.country_uuid - WHERE d.uuid = ?; - `; - - // Get the depot info - const [originDepot] = await db.exec(depotSql, [db.bid(shipmentDetails.origin_depot_uuid)]); - const odes = originDepot.description; - if (typeof odes === 'string') { - originDepot.description = odes.split('\n'); - } - shipmentDetails.origin_depot = originDepot; - - const [destinationDepot] = await db.exec(depotSql, [db.bid(shipmentDetails.destination_depot_uuid)]); - const ddes = destinationDepot.description; - if (typeof ddes === 'string') { - destinationDepot.description = ddes.split('\n'); - } - shipmentDetails.destination_depot = destinationDepot; - - // Compute totals - records.forEach(row => { - row.cost = row.quantity_sent * row.unit_price; - row.weight = row.quantity_sent * row.unit_weight; - }); - shipmentDetails.totalCost = records.reduce((agg, row) => agg + row.cost, 0); - shipmentDetails.totalQuantity = records.reduce((agg, row) => agg + row.quantity_sent, 0); - shipmentDetails.totalWeight = records.reduce((agg, row) => agg + row.weight, 0); - - shipmentDetails.barcode = barcode.generate(identifiers.SHIPMENT.key, shipmentDetails.uuid); - - const data = { - shipment : shipmentDetails, - records, - date : new Date(), - }; - - const result = await report.render(data); - res.set(result.headers).send(result.report); - } catch (e) { - next(e); - } -} - -exports.getShipmentOverview = getShipmentOverview; diff --git a/server/controllers/asset_management/shipment/shipment.js b/server/controllers/asset_management/shipment/shipment.js index 58d4093fb1..db407b9bbd 100644 --- a/server/controllers/asset_management/shipment/shipment.js +++ b/server/controllers/asset_management/shipment/shipment.js @@ -767,6 +767,7 @@ async function getPackingList(identifier) { l.label AS lot_label, sv.wac AS unit_price, i.code AS inventory_code, i.text AS inventory_label, i.is_asset, iu.text AS unit_type, + sc.label AS container_label, dm.text AS reference FROM shipment sh JOIN shipment_status ss ON ss.id = sh.status_id @@ -776,8 +777,10 @@ async function getPackingList(identifier) { JOIN inventory_unit iu ON iu.id = i.unit_id JOIN stock_value sv ON sv.inventory_uuid = i.uuid JOIN user u ON u.id = sh.created_by + LEFT JOIN shipment_container sc ON sc.uuid = shi.container_uuid JOIN document_map dm ON dm.uuid = sh.uuid WHERE sh.uuid = ? + ORDER BY container_label, inventory_label, lot_label `; return db.exec(sql, [db.bid(identifier)]); diff --git a/server/controllers/asset_management/shipment/shipment_containers.js b/server/controllers/asset_management/shipment/shipment_containers.js index 0952775a3a..25f76913ae 100644 --- a/server/controllers/asset_management/shipment/shipment_containers.js +++ b/server/controllers/asset_management/shipment/shipment_containers.js @@ -6,7 +6,7 @@ const NotFound = require('../../../lib/errors/NotFound'); const containerSql = ` SELECT BUID(sc.uuid) AS uuid, sc.label, BUID(sc.shipment_uuid) AS shipment_uuid, - sc.container_type_id, scType.text AS container_type + sc.weight, sc.container_type_id, scType.text AS container_type FROM shipment_container AS sc JOIN shipment_container_types AS scType ON scType.id = sc.container_type_id `; @@ -32,6 +32,7 @@ async function list(req, res, next) { try { const { params } = req; const filters = getFilters(params); + filters.setOrder('ORDER BY sc.label'); const query = filters.applyQuery(containerSql); const queryParameters = filters.parameters(); const result = await db.exec(query, queryParameters); @@ -41,6 +42,16 @@ async function list(req, res, next) { } } +async function containersForShipment(shipmentUuid) { + const params = { shipment_uuid : shipmentUuid }; + const filters = getFilters(params); + filters.setOrder('ORDER BY sc.label'); + const query = filters.applyQuery(containerSql); + const queryParameters = filters.parameters(); + const result = await db.exec(query, queryParameters); + return result; +} + async function details(req, res, next) { try { const { params } = req; // includes 'uuid' @@ -63,6 +74,9 @@ async function create(req, res, next) { label : params.label, container_type_id : params.container_type_id, }; + if (params.weight) { + container.weight = params.weight; + } if (params.shipment_uuid) { container.shipment_uuid = db.bid(params.shipment_uuid); } @@ -133,6 +147,7 @@ async function listTypes(req, res, next) { module.exports = { list, details, + containersForShipment, create, update, deleteContainer, diff --git a/server/models/migrations/next/migrate.sql b/server/models/migrations/next/migrate.sql index 04c531a250..d2671b5771 100644 --- a/server/models/migrations/next/migrate.sql +++ b/server/models/migrations/next/migrate.sql @@ -19,6 +19,7 @@ CREATE TABLE `shipment_container` ( `label` VARCHAR(100) NOT NULL, `shipment_uuid` BINARY(16) NOT NULL, `container_type_id` TINYINT(3) UNSIGNED NOT NULL, + `weight` FLOAT NOT NULL DEFAULT 0, `date_sent` DATETIME, `date_received` DATETIME, PRIMARY KEY (`uuid`), diff --git a/server/models/schema.sql b/server/models/schema.sql index 37ce589fd9..7b2e5fe5db 100644 --- a/server/models/schema.sql +++ b/server/models/schema.sql @@ -2753,6 +2753,7 @@ CREATE TABLE `shipment_container` ( `label` VARCHAR(100) NOT NULL, `shipment_uuid` BINARY(16) NOT NULL, `container_type_id` TINYINT(3) UNSIGNED NOT NULL, + `weight` FLOAT NOT NULL DEFAULT 0, `date_sent` DATETIME, `date_received` DATETIME, PRIMARY KEY (`uuid`), diff --git a/test/integration-stock/shipmentContainer.js b/test/integration-stock/shipmentContainer.js index c7430fe01a..bfca57eaf8 100644 --- a/test/integration-stock/shipmentContainer.js +++ b/test/integration-stock/shipmentContainer.js @@ -34,6 +34,7 @@ describe('(/shipment_containers) the shipments containers API', () => { const container11 = { uuid : helpers.uuid(), label : 'Ship1-Cont1', + weight : 2.3, shipment_uuid : shipment1.uuid, container_type_id : 1, }; @@ -78,8 +79,15 @@ describe('(/shipment_containers) the shipments containers API', () => { it('POST /shipment_containers create container 1 for the 1st shipment', () => { return agent.post('/shipment_containers') .send(container11) - .then((res) => { - helpers.api.created(res); + .then((res1) => { + helpers.api.created(res1); + // Reload the new container to verify creation + return agent.get(`/shipment_containers/${container11.uuid}/details`); + }) + .then(res2 => { + expect(res2.body.uuid).to.be.eq(container11.uuid); + expect(res2.body.label).to.be.eq(container11.label); + expect(res2.body.weight).to.be.eq(container11.weight); }) .catch(helpers.handler); });