From a380a7846ec95f293052014b30de935ee6fc4b00 Mon Sep 17 00:00:00 2001 From: Nicolas Malin Date: Tue, 7 May 2024 15:21:05 +0200 Subject: [PATCH] Improved: Convert InvoiceServices.xml from mini-lang to Groovy (OFBIZ-13085) Convert following services to groovy : * createInvoice * copyInvoice * getInvoice * updateInvoice * setInvoiceStatus * copyInvoiceToTemplate * checkInvoiceStatusInProgress * createInvoiceItem * updateInvoiceItem * removeInvoiceItem * sendInvoicePerEmail * autoGenerateInvoiceFromExistingInvoice * cancelInvoice * getInvoiceRunningTotal * addTaxOnInvoice * getInvoicesFilterByAssocType * removeInvoiceItemAssocOnCancelInvoice * resetOrderItemBillingAndOrderAdjustmentBillingOnCancelInvoice * massChangeInvoiceStatus * createInvoiceFromOrder * isInvoiceInForeignCurrency * removePaymentApplication (move to payment package) These services to entity-auto : * createInvoiceRole * removeInvoiceRole * createInvoiceTerm * createInvoiceContent * updateInvoiceContent Introduce new services : * checkInvoiceStatusInProgress * createInvoiceContentAndUpdateContent * updateInvoiceContentAndContent checkInvoiceStatusInProgress have been added as permission service to autorize or not edit service on invoice. createInvoiceContentAndUpdateContent and updateInvoiceContentAndContent have be added to manage both entity InvoiceContent and Content, and let createInvoiceContent and updateInvoiceContent as crud service --- .../accounting/config/AccountingUiLabels.xml | 22 +- .../minilang/invoice/InvoiceServices.xml | 895 ------------------ .../servicedef/services_invoice.xml | 339 +++---- .../servicedef/services_payment.xml | 4 +- .../accounting/AutoAcctgInvoiceTests.groovy | 7 +- .../accounting/InvoicePerShipmentTests.groovy | 2 +- .../invoice/InvoiceServicesScript.groovy | 530 ++++++++++- .../accounting/payment/PaymentServices.groovy | 60 ++ .../webapp/accounting/WEB-INF/controller.xml | 2 +- 9 files changed, 779 insertions(+), 1082 deletions(-) delete mode 100644 applications/accounting/minilang/invoice/InvoiceServices.xml diff --git a/applications/accounting/config/AccountingUiLabels.xml b/applications/accounting/config/AccountingUiLabels.xml index 3d3e4ddf7c7..f4e0391f495 100644 --- a/applications/accounting/config/AccountingUiLabels.xml +++ b/applications/accounting/config/AccountingUiLabels.xml @@ -8500,17 +8500,17 @@ 發票[${invoiceId}]金額合計為0....無法應用... - يمكن تحديث الفاتورة فقط عندما تكون الحالة قيد الإنجاز... الحالة الحالية: ${lookedUpValue.statusId} - Kann nur Rechnungen im Status "in Bearbeitung" aktualisieren... aktueller Status: ${lookedUpValue.statusId} - Can only update Invoice, when status is in-process...current status: ${lookedUpValue.statusId} - Sólo puede actualizar la factura, cuando el estado está 'En Proceso'. Estado actual: ${lookedUpValue.statusId} - Il n'est possible de mettre à jour une facture que si son statut est "en cours", actuellement il est ${lookedUpValue.statusId} - La fattura può essere aggiornata solo quando lo stato è in-corso...stato attuale: ${lookedUpValue.statusId} - 処理中の請求書のみ更新可能です...現在のステータス: ${lookedUpValue.statusId} - Só é possível atualizar fatura quando o estado é "em processo"... estado atual: ${lookedUpValue.statusId} - Chỉ có thể cập nhật Hóa đơn khi trạng thái là 'Đang xử lý'... trạng thái hiện tại là: ${lookedUpValue.statusId} - 只有在状态为处理中(in-process)时才能更新发票(Invoice),当前状态:${lookedUpValue.statusId} - 只有在狀態為處理中(in-process)時才能更新發票(Invoice),目前狀態:${lookedUpValue.statusId} + يمكن تحديث الفاتورة فقط عندما تكون الحالة قيد الإنجاز... الحالة الحالية: ${statusId} + Kann nur Rechnungen im Status "in Bearbeitung" aktualisieren... aktueller Status: ${statusId} + Can only update Invoice, when status is in-process...current status: ${statusId} + Sólo puede actualizar la factura, cuando el estado está 'En Proceso'. Estado actual: ${statusId} + Il n'est possible de mettre à jour une facture que si son statut est "en cours", actuellement il est ${statusId} + La fattura può essere aggiornata solo quando lo stato è in-corso...stato attuale: ${statusId} + 処理中の請求書のみ更新可能です...現在のステータス: ${statusId} + Só é possível atualizar fatura quando o estado é "em processo"... estado atual: ${statusId} + Chỉ có thể cập nhật Hóa đơn khi trạng thái là 'Đang xử lý'... trạng thái hiện tại là: ${statusId} + 只有在状态为处理中(in-process)时才能更新发票(Invoice),当前状态:${statusId} + 只有在狀態為處理中(in-process)時才能更新發票(Invoice),目前狀態:${statusId} الفواتير diff --git a/applications/accounting/minilang/invoice/InvoiceServices.xml b/applications/accounting/minilang/invoice/InvoiceServices.xml deleted file mode 100644 index 166b166d241..00000000000 --- a/applications/accounting/minilang/invoice/InvoiceServices.xml +++ /dev/null @@ -1,895 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/applications/accounting/servicedef/services_invoice.xml b/applications/accounting/servicedef/services_invoice.xml index 4843691a02c..bdfd7215f65 100644 --- a/applications/accounting/servicedef/services_invoice.xml +++ b/applications/accounting/servicedef/services_invoice.xml @@ -29,8 +29,8 @@ under the License. location="component://accounting/src/main/groovy/org/apache/ofbiz/accounting/invoice/InvoiceServicesScript.groovy" invoke="getNextInvoiceId"> Get the Next Invoice ID According to Settings on the PartyAcctgPreference Entity for the given Party - - + + - + Create Invoice Record - - - + + + + + - + Create Invoice Record/items from an existing invoice - + - + Retrieve an existing Invoice/Items - + - + Update an existing Invoice Record - - + + - + Set the Invoice Status - - - + + + - - Save a Invoice data to a template . + + Save a Invoice data to a template. - - + + + + + Check if the invoiceStatus is in progress + + + - + Create a new Invoice Item Record - - + + - + Update existing Invoice Item Record - - + + - + Remove an existing Invoice Item Record - - + + Create a Invoice Status Record - - + + - + Create a new Invoice Role Record - - + + + - + Remove an existing Invoice Role Record - - + + - + Create Invoice (Item) Term Record - - + + Update Invoice (Item) Term Record - + Delete Invoice (Item) Term Record - + @@ -177,7 +184,7 @@ under the License. Create an invoice from existing order using all order items orderId = The orderId to associate the invoice with - + - - + + @@ -200,7 +207,7 @@ under the License. returnId = The returnId to associate the invoice with billItems = List of ShipmentReceipts (for sales return) or ItemIssuance (for purchase return) to use for creating the invoice - + @@ -213,7 +220,7 @@ under the License. salesRepresentative: the invoice partyIdFrom - + - - + + Sets status of each invoice in the list of invoices to INVOICE_READY. - + @@ -241,14 +248,14 @@ under the License. one invoice for each order in the shipment will be created. invoicesCreated = List of invoiceIds which were created by this service - + Set invoice(s) to Ready from Shipment - + - + - + @@ -282,15 +289,15 @@ under the License. Create invoice(s) from a return Shipment invoicesCreated = List of invoiceIds which were created by this service - + - + Send an invoice per email - - - + + + @@ -299,159 +306,163 @@ under the License. Checks to see if the payments applied to an invoice total up to the invoice total; if so sets to PAID - + Create a ContactMech for an invoice - + Delete a ContactMech for an invoice - + Calculate the previously invoiced amount for an OrderAdjustment - - + + Update Invoice Item Type Record - + - + Scheduled service to generate Invoice from an existing Invoice - + - + Cancel Invoice - - + + - + calculate running total for selected Invoices - + - + - + Call Tax Calculate Service - + - + Filter invoices by invoiceItemAssocTypeId - - + + Create a InvoiceItemAssoc - + Update a InvoiceItemAssoc - + Delete a InvoiceItemAssoc - + - + Remove invoiceItemAssoc record on cancel invoice - + - + Reset OrderItemBilling and OrderAdjustmentBilling records on cancel invoice, so it is isn't considered invoiced any more by createInvoiceForOrder service - + - + Set status of invoices in bulk. - - - + + - + Create an invoice from existing order when invoicePerShipment is N - + - + Add Content To Invoice - + - - - + Update Content To Invoice - + - Remove Content From Invoice - - - - - - - - - - + - - - - - - + + Add Content To Invoice and update the content + + + + + + + update the content Invoice link and the content + + + + + + + + + + + + + + + + - - check if a invoice is in a foreign currency related to the accounting company. - - + + + check if an invoice is in a foreign currency related to the accounting company. + + - Import an invoice with invoiceitems in csv format + Import an invoice with invoice items in csv format - + @@ -502,16 +513,16 @@ under the License. Create a InvoiceAttribute - + Update a InvoiceAttribute - + Delete a InvoiceAttribute - + @@ -522,23 +533,23 @@ under the License. Update a InvoiceItemAssocType - + Delete a InvoiceItemAssocType - + Create a InvoiceNote - + Delete a InvoiceNote - + @@ -548,43 +559,43 @@ under the License. Update a InvoiceContentType - + Delete a InvoiceContentType - + Create InvoiceItemTypeAttr - + Update InvoiceItemTypeAttr - + Delete InvoiceItemTypeAttr - + Create InvoiceItemTypeMap - + Update InvoiceItemTypeMap - + Delete InvoiceItemTypeMap - + @@ -594,11 +605,11 @@ under the License. Update a InvoiceType - + Delete a InvoiceType - + diff --git a/applications/accounting/servicedef/services_payment.xml b/applications/accounting/servicedef/services_payment.xml index 842929a0dbe..c566f01485e 100644 --- a/applications/accounting/servicedef/services_payment.xml +++ b/applications/accounting/servicedef/services_payment.xml @@ -100,8 +100,8 @@ under the License. - + Delete a paymentApplication record. diff --git a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/AutoAcctgInvoiceTests.groovy b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/AutoAcctgInvoiceTests.groovy index ca9af5fcf69..f42a401cc4b 100644 --- a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/AutoAcctgInvoiceTests.groovy +++ b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/AutoAcctgInvoiceTests.groovy @@ -39,7 +39,7 @@ class AutoAcctgInvoiceTests extends OFBizTestCase { fromDate: UtilDateTime.nowTimestamp(), userLogin: userLogin ] - Map serviceResult = dispatcher.runSync('createInvoiceContent', serviceCtx) + Map serviceResult = dispatcher.runSync('createInvoiceContentAndUpdateContent', serviceCtx) assert ServiceUtil.isSuccess(serviceResult) GenericValue invoiceContent = from('InvoiceContent') @@ -53,7 +53,6 @@ class AutoAcctgInvoiceTests extends OFBizTestCase { void testCreateSimpleTextContentForInvoice() { Map serviceCtx = [ invoiceId: '1009', - contentId: '1001', contentTypeId: 'DOCUMENT', invoiceContentTypeId: 'COMMENTS', text: 'Content for invoice # 1009', @@ -65,7 +64,6 @@ class AutoAcctgInvoiceTests extends OFBizTestCase { GenericValue invoiceContent = from('InvoiceContent') .where('invoiceId', '1009', - 'contentId', '1001', 'invoiceContentTypeId', 'COMMENTS') .queryFirst() @@ -141,7 +139,8 @@ class AutoAcctgInvoiceTests extends OFBizTestCase { void testCreateInvoiceItem() { Map serviceCtx = [ invoiceId: '1003', - invoiceTypeId: 'PINV_FXASTPRD_ITEM', + invoiceItemTypeId: 'PINV_FXASTPRD_ITEM', + amount: 1, userLogin: userLogin ] Map serviceResult = dispatcher.runSync('createInvoiceItem', serviceCtx) diff --git a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/InvoicePerShipmentTests.groovy b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/InvoicePerShipmentTests.groovy index b4baa323b30..dae3ada723f 100644 --- a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/InvoicePerShipmentTests.groovy +++ b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/accounting/InvoicePerShipmentTests.groovy @@ -135,7 +135,7 @@ class InvoicePerShipmentTests extends OFBizTestCase { // Step 3 GenericValue orderHeader = from('OrderHeader').where('orderTypeId', 'SALES_ORDER').orderBy('-entryDate').queryFirst() - logInfo('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx : ' + orderHeader) + logInfo('===== >>> orderHeader : ' + orderHeader) if (invoicePerShipment) { // if this value is available that means we need to set this on the order diff --git a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/invoice/InvoiceServicesScript.groovy b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/invoice/InvoiceServicesScript.groovy index 19543f01e16..47ede9c29b7 100644 --- a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/invoice/InvoiceServicesScript.groovy +++ b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/invoice/InvoiceServicesScript.groovy @@ -18,16 +18,23 @@ */ package org.apache.ofbiz.accounting.invoice +import org.apache.ofbiz.accounting.util.UtilAccounting import org.apache.ofbiz.base.util.Debug import org.apache.ofbiz.base.util.UtilDateTime +import org.apache.ofbiz.base.util.UtilFormatOut import org.apache.ofbiz.base.util.UtilValidate import org.apache.ofbiz.entity.GenericValue +import org.apache.ofbiz.entity.condition.EntityCondition +import org.apache.ofbiz.entity.condition.EntityConditionBuilder +import org.apache.ofbiz.entity.util.EntityTypeUtil +import org.apache.ofbiz.entity.util.EntityUtil +import org.apache.ofbiz.entity.util.EntityUtilProperties import org.apache.ofbiz.service.ServiceUtil import java.sql.Timestamp Map getNextInvoiceId() { - result = success() + Map result = success() // try to find PartyAcctgPreference for parameters.partyId, see if we need any special invoice number sequencing GenericValue partyAcctgPreference = from('PartyAcctgPreference').where(parameters).queryOne() @@ -85,7 +92,7 @@ Map getNextInvoiceId() { } Map invoiceSequenceEnforced() { - result = success() + Map result = success() logInfo('In createInvoice sequence enum Enforced') GenericValue partyAcctgPreference = parameters.partyAcctgPreference @@ -103,8 +110,6 @@ Map invoiceSequenceEnforced() { } Map invoiceSequenceRestart() { - result = success() - logInfo('In createInvoice sequence enum Enforced') GenericValue partyAcctgPreference = parameters.partyAcctgPreference //this is sequential sequencing, we can't skip a number, also it must be a unique sequence per partyIdFrom @@ -134,3 +139,520 @@ Map invoiceSequenceRestart() { return success(invoiceId: "${curYearString}-${partyAcctgPreference.lastInvoiceNumber}") } +/** + * Create a new Invoice + * @return Success response containing the invoiceId, error response otherwise. + */ +Map createInvoice() { + if (!parameters.invoiceId) { + Map serviceResult = run service: 'getNextInvoiceId', with: [*: parameters, + partyId: parameters.partyIdFrom] + parameters.invoiceId = serviceResult.invoiceId + } + GenericValue party = from('Party').where(parameters).queryOne() + if (party?.preferredCurrencyUomId) { + parameters.currencyUomId = party.preferredCurrencyUomId + } + GenericValue invoice = makeValue('Invoice', parameters) + invoice.create() + run service: 'createInvoiceStatus', with: parameters + return success([invoiceId: invoice.invoiceId]) +} + +/** + * Retrieve an invoice and the items + * @return Success response containing the invoice and items, failure response otherwise. + */ +Map getInvoice() { + GenericValue invoice = from('Invoice').where(parameters).queryOne() + if (!invoice) { + return failure(label('AccountingUiLabels', 'AccountingInvoiceNotFound', parameters)) + } + return success([invoice: invoice, + invoiceItems: invoice.getRelated('InvoiceItem', null, null, false)]) +} + +/** + * Update the header of an existing Invoice + * @return Success response if invoice updated, error response otherwise. + */ +Map updateInvoice() { + GenericValue invoice = from('Invoice').where(parameters).queryOne() + if (!invoice) { + return error(label('AccountingUiLabels', 'AccountingInvoiceNotFound', parameters)) + } + if (invoice.statusId != 'INVOICE_IN_PROCESS') { + return error(label('AccountingUiLabels', 'AccountingInvoiceUpdateOnlyWithInProcessStatus', [statustId: invoice.statusId])) + } + + // only save if something has changed, do not update status here + // update all non status and key fields + GenericValue lookedInvoice = invoice.clone() + invoice.setNonPKFields([*: parameters, statustId: 'INVOICE_IN_PROCESS'], false) + if (lookedInvoice != invoice) { + invoice.store() + } + + // check if there is a requested status change if yes call invoice status update service + if (parameters.statusId && parameters.statusId != 'INVOICE_IN_PROCESS') { + run service: 'setInvoiceStatus', with: [invoiceId: invoice.invoiceId, + statustId: parameters.statustId] + } + return success() +} + +/** + * Create a new Invoice from an existing invoice + * @return Success response containing the invoiceId, error response otherwise. + */ +Map copyInvoice() { + Map serviceResult = run service: 'getInvoice', with: [invoiceId: parameters.invoiceIdToCopyFrom] + GenericValue invoice = serviceResult.invoice + List invoiceItems = serviceResult.invoiceItems + invoice.invoiceTypeId = parameters.invoiceTypeId ?: invoice.invoiceTypeId + serviceResult = run service: 'createInvoice', with: [*: invoice.getAllFields(), + invoiceId: null] + String newInvoiceId = serviceResult.invoiceId + invoiceItems.each { + run service: 'createInvoiceItem', with: [*: it.getAllFields(), + invoiceId: newInvoiceId] + } + return success([invoiceId: newInvoiceId]) +} + +/** + * Copy a invoice to a InvoiceType starting with 'template' + * @return Success response containing the invoiceId, error response otherwise. + */ +Map copyInvoiceToTemplate() { + String invoiceTypeId = parameters.invoiceTypeId + Map switchType = [SALES_INVOICE: 'SALES_INV_TEMPLATE', + PURCHASE_INVOICE: 'PUR_INV_TEMPLATE'] + run service: 'copyInvoice', with: [*: parameters, + invoiceIdToCopyFrom: parameters.invoiceId, + invoiceTypeId: switchType.get(invoiceTypeId) ?: invoiceTypeId] +} + +/** + * Set The Invoice Status + * @return Success response after status stored, error response otherwise. + */ +Map setInvoiceStatus() { + GenericValue invoice = from('Invoice').where(parameters).queryOne() + if (!invoice) { + return error(label('AccountingUiLabels', 'AccountingInvoiceNotFound', parameters)) + } + String oldStatusId = invoice.statusId + String invoiceTypeId = invoice.invoiceTypeId + Map returnResult = [oldStatusId: oldStatusId, invoiceTypeId: invoiceTypeId] + if (oldStatusId == parameters.statusId) { + return success(returnResult) + } + + if (from('StatusValidChange') + .where(statusId: oldStatusId, statusIdTo: parameters.statusId) + .queryCount() == 0) { + return error(label('AccountingUiLabels', 'AccountingPSInvalidStatusChange')) + } + + // if new status is paid check if the complete invoice is applied + if (parameters.statusId == 'INVOICE_PAID') { + BigDecimal notApplied = InvoiceWorker.getInvoiceNotApplied(invoice) + if (notApplied != 0) { + return error(label('AccountingUiLabels', 'AccountingInvoiceCannotChangeStatusToPaid')) + } + } + + // if it's OK to mark invoice paid, use parameters for paidDate + invoice.paidDate = parameters.paidDate ?: UtilDateTime.nowTimestamp() + + if (parameters.statusId == 'INVOICE_READY' && invoice.paidDate) { + invoice.paidDate = null + } + invoice.statusId = parameters.statusId + invoice.store() + + run service: 'createInvoiceStatus', with: [invoiceId: invoice.invoiceId, + statusId: invoice.statusId, + statusDate: parameters.statusDate] + + // if the invoice is a payrol invoice, create the payment in the not-paid status + // TODO the next part need to move on dedicate service + if (invoiceTypeId == 'PAYROL_INVOICE' && + ['INVOICE_APPROVED', 'INVOICE_READY'].contains(invoice.statusId)) { + // only generate payment if no application exist yet + List paymentApplications = invoice.getRelated('PaymentApplication', null, null, false) + if (!paymentApplications) { + BigDecimal amount = InvoiceWorker.getInvoiceTotal(invoice) + Map serviceResult = run service: 'createPayment', with: [partyIdFrom: invoice.partyId, + partyIdTo: invoice.partyIdFrom, + paymentMethodTypeId: 'COMPANY_CHECK', + paymentTypeId: 'PAYROL_PAYMENT', + statusId: 'PMNT_NOT_PAID', + currencyUomId: invoice.currencyUomId, + amount: amount] + run service: 'createPaymentApplication', with: [invoiceId: invoice.invoiceId, + paymentId: serviceResult.paymentId, + amountApplied: amount] + } + } + return success(returnResult) +} + +/** + * Check if the invoiceStatus is in progress + * @return Success response containing hasPermission to edit, error response otherwise. + */ +Map checkInvoiceStatusInProgress() { + GenericValue invoice = from('Invoice').where(parameters).cache().queryOne() + boolean hasPermission = invoice && invoice.statusId == 'INVOICE_IN_PROCESS' + return success([hasPermission: hasPermission]) +} + +/** + * Service run after cancel an invoice + * @return Success response containing the invoiceTypeId cancelled, error response otherwise. + */ +Map cancelInvoice() { + GenericValue invoice = from('Invoice').where(parameters).cache().queryOne() + if (!invoice) { + return error(label('AccountingUiLabels', 'AccountingInvoiceNotFound', parameters)) + } + invoice.getRelated('PaymentApplication', null, null, false).each { + GenericValue payment = it.getRelatedOne('Payment', false) + if (payment.statusId == 'PMNT_CONFIRMED') { + run service: 'setPaymentStatus', with: [paymentId: payment.paymentId, + statusId: UtilAccounting.isReceipt(payment) ? 'PMNT_RECEIVED' : 'PMNT_SENT'] + } + run service: 'removePaymentApplication', with: [paymentApplicationId: it.paymentApplicationId] + } + return success([invoiceTypeId: invoice.invoiceTypeId]) +} + +/** + * Send an invoice per Email + * @return Success response + */ +Map sendInvoicePerEmail() { + Map emailParams = dispatcher.getDispatchContext() + .makeValidContext([*: parameters, + xslfoAttachScreenLocation: 'component://accounting/widget/AccountingPrintScreens.xml#InvoicePDF', + bodyParameters: [invoiceId: parameters.invoiceId, + userLogin: parameters.userLogin, + other: parameters.other] //to print in 'other currency' + ]) + dispatcher.runAsync('sendMailFromScreen', emailParams) + return success(label('AccountingUiLabels', 'AccountingEmailScheduledToSend')) +} + +/** + * Create a new Invoice Item + * @return Success response containing the invoiceItemSeqId, error response otherwise. + */ +Map createInvoiceItem() { + GenericValue invoiceItem = makeValue('InvoiceItem', parameters) + if (!invoiceItem.invoiceItemSeqId) { + delegator.setNextSubSeqId(invoiceItem, 'invoiceItemSeqId', 5, 1) + } + // if there is no amount and a productItem is supplied fill the amount(price) and description from the product record + // TODO: there are return adjustments now that make this code very broken. The check for price was added as a quick fix. + if (invoiceItem.productId) { + invoiceItem.quantity = invoiceItem.quantity ?: 1 + if (!invoiceItem.amount) { + GenericValue product = from('Product').where(parameters).cache().queryOne() + invoiceItem.description = product.description + Map serviceResult = run service: 'calculateProductPrice', with: [product: product] + invoiceItem.amount = serviceResult.price + } + } + if (invoiceItem.amount == null) { // accept 0 + return error(label('AccountingUiLabels', 'AccountingInvoiceAmountIsMandatory')) + } + invoiceItem.create() + return success([invoiceId: invoiceItem.invoiceId, + invoiceItemSeqId: invoiceItem.invoiceItemSeqId]) +} + +/** + * Update an existing Invoice Item + * @return Success response after updated, error response otherwise. + */ +Map updateInvoiceItem() { + GenericValue invoiceItem = from('InvoiceItem').where(parameters).queryOne() + if (!invoiceItem) { + return error(label('AccountingUiLabels', 'AccountingInvoiceItemNotFound', parameters)) + } + GenericValue lookedInvoiceItem = invoiceItem.clone() + invoiceItem.setNonPKFields(parameters, false) + + // check if the productNumber is updated, when yes retrieve product description and price + if (lookedInvoiceItem.productId != invoiceItem.productId) { + GenericValue product = from('Product').where(parameters).cache().queryOne() + invoiceItem.description = product.description + Map serviceResult = run service: 'calculateProductPrice', with: [product: product] + invoiceItem.amount = serviceResult.price + if (invoiceItem.amount == null) { + return error(label('AccountingUiLabels', 'AccountingInvoiceAmountIsMandatory')) + } + } + if (lookedInvoiceItem != invoiceItem) { + invoiceItem.store() + } + return success([invoiceId: invoiceItem.invoiceId, + invoiceItemSeqId: invoiceItem.invoiceItemSeqId]) +} + +/** + * Remove an existing Invoice Item + * @return Success response after remove, error response otherwise. + */ +Map removeInvoiceItem() { + GenericValue invoiceItem = from('InvoiceItem').where(parameters).queryOne() + if (!invoiceItem) { + return error(label('AccountingUiLabels', 'AccountingInvoiceItemNotFound', parameters)) + } + // check if there are specific item paymentApplications when yes remove those + invoiceItem.removeRelated('PaymentApplication') + invoiceItem.remove() + return success() +} + +/** + * Scheduled service to generate Invoice from an existing Invoice + */ +Map autoGenerateInvoiceFromExistingInvoice() { + Map switchType = [SALES_INV_TEMPLATE: 'SALES_INVOICE', + PUR_INV_TEMPLATE: 'PURCHASE_INVOICE'] + from('Invoice') + .where(recurrenceInfoId: parameters.recurrenceInfoId) + .queryList() + .each { + Map serviceResult = run service: 'copyInvoice', with: [*: it.getAllFields(), + invoiceIdToCopyFrom: it.invoiceId] + if (switchType.containsKey(it.invoiceTypeId)) { + String invoiceId = serviceResult.invoiceId + run service: 'updateInvoice', with: [invoiceId: invoiceId, + recurrenceInfoId: null, + invoiceTypeId: switchType(it.invoiceTypeId)] + } + } + return success() +} + +/** + * Calculate running total for Invoices + * @return Success response containing the invoiceRunningTotal, error response otherwise. + */ +Map getInvoiceRunningTotal() { + BigDecimal runningTotal = 0 + parameters.invoiceIds.each { + Map serviceResult = run service: 'getInvoicePaymentInfoList', with: [invoiceId: it] + if (serviceResult.invoicePaymentInfoList) { + runningTotal += serviceResult.invoicePaymentInfoList[0].outstandingAmount + } + } + Map serviceResult = run service: 'getPartyAccountingPreferences', with: parameters + Map partyAccountingPreference = serviceResult.partyAccountingPreference + String currencyUomId = partyAccountingPreference.baseCurrencyUomId ?: + EntityUtilProperties.getPropertyValue('general', 'currency.uom.id.default', 'USD', delegator) + return success([invoiceRunningTotal: UtilFormatOut.formatCurrency(runningTotal, currencyUomId, parameters.locale)]) +} + +/** + * Filter invoices by invoiceItemAssocTypeId + * @return Success response containing filteredInvoiceList, error response otherwise. + */ +Map getInvoicesFilterByAssocType() { + EntityCondition condition = new EntityConditionBuilder().AND { + EQUALS(invoiceItemAssocTypeId: parameters.invoiceItemAssocTypeId) + IN(invoiceIdFrom: parameters.invoiceList*.invoiceId) + } + List invoiceIds = from('InvoiceItemAssoc') + .where(condition) + .distinct() + .filterByDate() + .getFieldList('invoiceIdFrom') + return success([filteredInvoiceList: parameters.invoiceList.findAll { invoiceIds.contains(it.invoiceId) }]) +} + +/** + * Remove invoiceItemAssoc record on cancel invoice + * @return Success response after remove, error response otherwise. + */ +Map removeInvoiceItemAssocOnCancelInvoice() { + from('InvoiceItemAssoc') + .where(invoiceIdTo: parameters.invoiceId) + .queryList() + .each { + run service: 'deleteInvoiceItemAssoc', with: it.getAllFields() + } + return success() +} + +/** + * Reset OrderItemBilling and OrderAdjustmentBilling records on cancel invoice, + * so it is isn't considered invoiced any more by createInvoiceForOrder service + * @return Success response + */ +Map resetOrderItemBillingAndOrderAdjustmentBillingOnCancelInvoice() { + from('OrderItemBilling') + .where(invoiceId: parameters.invoiceId) + .queryList() + .each { + it.quantity = 0 + it.store() + } + from('OrderAdjustmentBilling') + .where(invoiceId: parameters.invoiceId) + .queryList() + .each { + it.amount = 0 + it.store() + } + return success() +} + +/** + * Service set status of Invoices in bulk. + * @return Success response + */ +Map massChangeInvoiceStatus() { + parameters.invoiceIds.each { + run service: 'setInvoiceStatus', with: [invoiceId: it, + statusId: parameters.statusId] + } + return success() +} + +/** + * Set Parameter And Call Tax Calculate Service + * @return Success response + */ +Map addTaxOnInvoice() { + GenericValue invoice = from('Invoice').where(parameters).cache().queryOne() + if (!invoice) { + return error(label('AccountingUiLabels', 'AccountingInvoiceNotFound', parameters)) + } + GenericValue shippingContact = from('PartyContactMechPurpose') + .where(partyId: invoice.partyId, + contactMechPurposeTypeId: 'SHIPPING_LOCATION') + .queryFirst() ?: + from('PartyContactMechPurpose') + .where(partyId: invoice.partyId, + contactMechPurposeTypeId: 'GENERAL_LOCATION') + .queryFirst() + if (!shippingContact) { + return error(label('AccountingUiLabels', 'AccountingTaxCannotCalculate')) + } + GenericValue postalAddress = from('PostalAddress').where(contactMechId: shippingContact.contactMechId).cache().queryOne() + Map addTaxMap = [billToPartyId: invoice.invoiceTypeId == 'SALES_INVOICE' ? invoice.partyId : invoice.partyIdFrom, + payToPartyId: invoice.partyIdFrom, + orderPromotionsAmount: 0, + orderShippingAmount: 0, + shippingAddress: postalAddress, + itemProductList: [], + itemAmountList: [], + itemPriceList: [], + itemQuantityList: [], + itemShippingList: []] + List invoiceItems = invoice.getRelated('InvoiceItem', null, null, false) + invoiceItems.each { + BigDecimal totalAmount = 0 + if (it.productId) { + addTaxMap.itemProductList << from('Product').where(productId: it.productId).cache().queryOne() + List promoAdjs = EntityUtil.filterByAnd(invoiceItems, + [productId: it.productId, + invoiceItemTypeId: 'ITM_PROMOTION_ADJ']) + totalAmount = it.amount * it.quantity + if (promoAdjs) { + totalAmount -= it.amount + } + } + addTaxMap.itemAmountList << totalAmount + addTaxMap.itemPriceList << it.amount + addTaxMap.itemQuantityList << it.quantity + addTaxMap.itemShippingList << 0 + } + if (!addTaxMap.itemProductList) { + return error(label('AccountingUiLabels', 'AccountingTaxProductIdCannotCalculate')) + } + Map serviceResult = run service: 'calcTax', with: addTaxMap + Map itemMap = [itemSeqIdList: [], + productList: []] + invoiceItems.findAll { it.productId }.each { + itemMap.itemSeqIdList << it.invoiceItemSeqId + itemMap.productList << it.productId + } + Long countItemId = -1 + serviceResult.itemAdjustments.each { + countItemId ++ + if (it) { + it.each { + run service: 'createInvoiceItem', with: [invoiceItemTypeId: invoice.invoiceTypeId == 'PURCHASE_INVOICE' ? + 'PITM_SALES_TAX' : 'ITM_SALES_TAX', + invoiceId: invoice.invoiceId, + overrideGlAccountId: it.overrideGlAccountId, + productId: itemMap.productList[countItemId], + taxAuthPartyId: it.taxAuthPartyId, + taxAuthGeoId: it.taxAuthGeoId, + amount: it.amount, + quantity: 1, + parentInvoiceItemSeqId: itemMap.itemSeqIdList[countItemId], + taxAuthorityRateSeqId: it.taxAuthorityRateSeqId, + description: it.comments] + } + } + } + serviceResult.orderAdjustments.each { + run service: 'createInvoiceItem', with: [invoiceItemTypeId: invoice.invoiceTypeId == 'PURCHASE_INVOICE' ? + 'PITM_SALES_TAX' : 'ITM_SALES_TAX', + invoiceId: invoice.invoiceId, + overrideGlAccountId: it.overrideGlAccountId, + taxAuthPartyId: it.taxAuthPartyId, + taxAuthGeoId: it.taxAuthGeoId, + amount: it.amount, + quantity: 1, + taxAuthorityRateSeqId: it.taxAuthorityRateSeqId, + description: it.comments] + } + return success() +} + +/** + * Create an invoice from existing order when invoicePerShipment is N + * @return Success response + */ +Map createInvoiceFromOrder() { + GenericValue order = from('OrderHeader').where(parameters).queryOne() + String invoicePerShipment = order?.invoicePerShipment ?: + EntityUtilProperties.getPropertyValue('accounting', 'create.invoice.per.shipment', 'N', delegator) + if (invoicePerShipment == 'N') { + List orderItemBillingItemsSeqIds = from('OrderItemBilling').where(orderId: order.orderId).getFieldList('orderItemSeqId') + if (!orderItemBillingItemsSeqIds) { + Map serviceResult = run service: 'createInvoiceForOrderAllItems', with: [orderId: order.orderId] + return serviceResult + } + List orderItems = from('OrderItem').where(orderId: order.orderId).queryList() + orderItems = orderItems.findAll { !orderItemBillingItemsSeqIds.contains(it.orderItemSeqId) } + Map serviceResult = run service: 'createInvoiceForOrder', with: [orderId: order.orderId, + billItems: orderItems] + return serviceResult + } + return success() +} + +/** + * check if a invoice is in a foreign currency related to the accounting company. + * @return Success response + */ +Map isInvoiceInForeignCurrency() { + GenericValue invoice = from('Invoice').where(parameters).cache().queryOne() + if (!invoice) { + return error(label('AccountingUiLabels', 'AccountingInvoiceNotFound', parameters)) + } + String partyId = EntityTypeUtil.hasParentType(delegator, 'InvoiceType', 'invoiceTypeId', + invoice.invoiceTypeId, 'parentTypeId', 'PURCHASE_INVOICE') ? + invoice.partyId : invoice.partyIdFrom + Map serviceResult = run service: 'getPartyAccountingPreferences', with: [organizationPartyId: partyId] + return success([isForeign: invoice.currencyUomId == serviceResult.baseCurrencyUomId]) +} diff --git a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/payment/PaymentServices.groovy b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/payment/PaymentServices.groovy index 11d3b3cd93f..8e536328b70 100644 --- a/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/payment/PaymentServices.groovy +++ b/applications/accounting/src/main/groovy/org/apache/ofbiz/accounting/payment/PaymentServices.groovy @@ -960,3 +960,63 @@ Map createMatchingPaymentApplication() { } return success() } + +/** + * Remove an existing payment application + * @return Success response after remove, error response otherwise. + */ +Map removePaymentApplication() { + GenericValue paymentApplication = from('PaymentApplication').where(parameters).queryOne() + if (!paymentApplication) { + return error(label('AccountingUiLabels', 'AccountingPaymentApplicationNotFound', parameters)) + } + Map paymentApplicationFields = paymentApplication.getAllFields() + + String toMessage = '' + // check payment + if (paymentApplication.paymentId) { + GenericValue payment = from('Payment').where(paymentId: paymentApplication.paymentId).queryOne() + if (payment.statusId == 'PMNT_CONFIRMED') { + return error(label('AccountingUiLabels', 'AccountingPaymentApplicationCannotRemovedWithConfirmedStatus')) + } + } + + // check invoice + if (paymentApplication.invoiceId) { + // if the invoice is already PAID, then set it back to READY and clear out the paidDate + GenericValue invoice = from('Invoice').where(invoiceId: paymentApplication.invoiceId).queryOne() + if (invoice.statusId == 'INVOICE_PAID') { + run service: 'setInvoiceStatus', with: [invoiceId: paymentApplication.invoiceId, + statustId: 'INVOICE_READY'] + } + toMessage = label('AccountingUiLabels', 'AccountingPaymentApplToInvoice', paymentApplicationFields) + } + + // check invoice item + if (paymentApplication.invoiceItemSeqId) { + toMessage = label('AccountingUiLabels', 'AccountingApplicationToInvoiceItem', paymentApplicationFields) + } + + // check toPayment + if (paymentApplication.toPaymentId) { + GenericValue toPayment = from('Payment').where(paymentId: paymentApplication.toPaymentId).queryOne() + if (toPayment.statusId == 'PMNT_CONFIRMED') { + return error(label('AccountingUiLabels', 'AccountingPaymentApplicationCannotRemovedWithConfirmedStatus')) + } + toMessage = label('AccountingUiLabels', 'AccountingPaymentApplToPayment', paymentApplicationFields) + } + + // check billing account + if (paymentApplication.billingAccountId) { + toMessage = label('AccountingUiLabels', 'AccountingPaymentApplToBillingAccount', paymentApplicationFields) + } + + // check tax authority + if (paymentApplication.taxAuthGeoId) { + toMessage = label('AccountingUiLabels', 'AccountingPaymentApplToTaxAuth', paymentApplicationFields) + } + + // finally delete application + paymentApplication.remove() + return success(label('AccountingUiLabels', 'AccountingPaymentApplRemoved') + ' ' + toMessage, paymentApplicationFields) +} diff --git a/applications/accounting/webapp/accounting/WEB-INF/controller.xml b/applications/accounting/webapp/accounting/WEB-INF/controller.xml index 67ab3dd6723..717abfdad38 100644 --- a/applications/accounting/webapp/accounting/WEB-INF/controller.xml +++ b/applications/accounting/webapp/accounting/WEB-INF/controller.xml @@ -2546,7 +2546,7 @@ under the License. - +