Java tutorial
/* * Version: 1.0 * * The contents of this file are subject to the OpenVPMS License Version * 1.0 (the 'License'); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.openvpms.org/license/ * * Software distributed under the License is distributed on an 'AS IS' basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * Copyright 2015 (C) OpenVPMS Ltd. All Rights Reserved. */ package org.openvpms.web.workspace.customer.charge; import nextapp.echo2.app.Component; import nextapp.echo2.app.Label; import org.apache.commons.lang.ObjectUtils; import org.openvpms.archetype.rules.finance.account.CustomerAccountArchetypes; import org.openvpms.archetype.rules.finance.invoice.ChargeItemDocumentLinker; import org.openvpms.archetype.rules.finance.tax.TaxRuleException; import org.openvpms.archetype.rules.math.MathRules; import org.openvpms.archetype.rules.patient.reminder.ReminderRules; import org.openvpms.archetype.rules.product.ProductArchetypes; import org.openvpms.archetype.rules.stock.StockRules; import org.openvpms.component.business.domain.im.act.Act; import org.openvpms.component.business.domain.im.act.FinancialAct; import org.openvpms.component.business.domain.im.common.Entity; import org.openvpms.component.business.domain.im.common.EntityRelationship; import org.openvpms.component.business.domain.im.common.IMObject; import org.openvpms.component.business.domain.im.common.IMObjectReference; import org.openvpms.component.business.domain.im.party.Party; import org.openvpms.component.business.domain.im.product.Product; import org.openvpms.component.business.domain.im.product.ProductPrice; import org.openvpms.component.business.service.archetype.ArchetypeServiceException; import org.openvpms.component.business.service.archetype.IArchetypeService; import org.openvpms.component.business.service.archetype.helper.ActBean; import org.openvpms.component.business.service.archetype.helper.EntityBean; import org.openvpms.component.business.service.archetype.helper.IMObjectBean; import org.openvpms.component.business.service.archetype.helper.TypeHelper; import org.openvpms.component.system.common.exception.OpenVPMSException; import org.openvpms.web.component.edit.Editor; import org.openvpms.web.component.im.edit.IMObjectCollectionEditorFactory; import org.openvpms.web.component.im.edit.IMObjectEditor; import org.openvpms.web.component.im.edit.act.ActRelationshipCollectionEditor; import org.openvpms.web.component.im.edit.act.ClinicianParticipationEditor; import org.openvpms.web.component.im.edit.act.ParticipationEditor; import org.openvpms.web.component.im.edit.reminder.ReminderEditor; import org.openvpms.web.component.im.layout.ArchetypeNodes; import org.openvpms.web.component.im.layout.DefaultLayoutContext; import org.openvpms.web.component.im.layout.IMObjectLayoutStrategy; import org.openvpms.web.component.im.layout.LayoutContext; import org.openvpms.web.component.im.patient.PatientActEditor; import org.openvpms.web.component.im.patient.PatientParticipationEditor; import org.openvpms.web.component.im.product.BatchParticipationEditor; import org.openvpms.web.component.im.product.FixedPriceEditor; import org.openvpms.web.component.im.product.ProductParticipationEditor; import org.openvpms.web.component.im.util.IMObjectSorter; import org.openvpms.web.component.im.util.LookupNameHelper; import org.openvpms.web.component.im.view.ComponentState; import org.openvpms.web.component.property.CollectionProperty; import org.openvpms.web.component.property.Modifiable; import org.openvpms.web.component.property.ModifiableListener; import org.openvpms.web.component.property.Property; import org.openvpms.web.component.property.Validator; import org.openvpms.web.component.util.ErrorHelper; import org.openvpms.web.echo.dialog.ConfirmationDialog; import org.openvpms.web.echo.dialog.PopupDialogListener; import org.openvpms.web.echo.factory.LabelFactory; import org.openvpms.web.echo.factory.RowFactory; import org.openvpms.web.echo.focus.FocusGroup; import org.openvpms.web.echo.focus.FocusHelper; import org.openvpms.web.echo.style.Styles; import org.openvpms.web.resource.i18n.Messages; import org.openvpms.web.system.ServiceHelper; import org.openvpms.web.workspace.customer.PriceActItemEditor; import org.openvpms.web.workspace.patient.history.PatientInvestigationActEditor; import org.openvpms.web.workspace.patient.mr.PatientMedicationActEditor; import org.openvpms.web.workspace.patient.mr.PrescriptionMedicationActEditor; import org.openvpms.web.workspace.patient.mr.Prescriptions; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import static org.openvpms.archetype.rules.math.MathRules.ONE_HUNDRED; import static org.openvpms.archetype.rules.product.ProductArchetypes.MEDICATION; import static org.openvpms.archetype.rules.product.ProductArchetypes.MERCHANDISE; import static org.openvpms.archetype.rules.product.ProductArchetypes.SERVICE; import static org.openvpms.archetype.rules.product.ProductArchetypes.TEMPLATE; import static org.openvpms.archetype.rules.stock.StockArchetypes.STOCK_LOCATION_PARTICIPATION; /** * An editor for {@link org.openvpms.component.business.domain.im.act.Act}s which have an archetype of * <em>act.customerAccountInvoiceItem</em>, * <em>act.customerAccountCreditItem</em> * or <em>act.customerAccountCounterItem</em>. * * @author Tim Anderson */ public abstract class CustomerChargeActItemEditor extends PriceActItemEditor { /** * Dispensing act editor. May be {@code null} */ private DispensingActRelationshipCollectionEditor dispensing; /** * Investigation act editor. May be {@code null} */ private ActRelationshipCollectionEditor investigations; /** * Reminders act editor. May be {@code null} */ private ActRelationshipCollectionEditor reminders; /** * The medication, investigation and reminder act editor manager. */ private EditorQueue editorQueue; /** * Listener for changes to the quantity. */ private final ModifiableListener quantityListener; /** * Listener for changes to the medication act. */ private final ModifiableListener dispensingListener; /** * Listener for changes to the start time. */ private final ModifiableListener startTimeListener; /** * Listener for changes to the fixed price, quantity and unit price, to update the discount. */ private final ModifiableListener discountListener; /** * Listener for changes to the total, so the tax amount can be recalculated. */ private final ModifiableListener totalListener; /** * Listener for changes to the batch. */ private final ModifiableListener batchListener; /** * Stock rules. */ private StockRules rules; /** * Reminder rules. */ private ReminderRules reminderRules; /** * Selling units label. */ private Label sellingUnits; /** * The prescriptions. */ private Prescriptions prescriptions; /** * If {@code true}, prompt to use prescriptions. */ private boolean promptForPrescription = true; /** * If {@code true}, enable medication editing to be cancelled when it is being dispensed from a prescription. */ private boolean cancelPrescription; /** * The charge context. */ private ChargeContext chargeContext; /** * Dispensing node name. */ private static final String DISPENSING = "dispensing"; /** * Reminders node name. */ private static final String REMINDERS = "reminders"; /** * Returned quantity node name. */ private static final String RETURNED_QUANTITY = "returnedQuantity"; /** * Received quantity node name. */ private static final String RECEIVED_QUANTITY = "receivedQuantity"; /** * Pharmacy order nodes. */ private static final String[] ORDER_NODES = { RECEIVED_QUANTITY, RETURNED_QUANTITY }; /** * Investigations node name. */ private static final String INVESTIGATIONS = "investigations"; /** * Start time node name. */ private static final String START_TIME = "startTime"; /** * Discount node name. */ private static final String DISCOUNT = "discount"; /** * Quantity node name. */ private static final String QUANTITY = "quantity"; /** * Fixed cost node name. */ private static final String FIXED_COST = "fixedCost"; /** * Fixed price node name. */ private static final String FIXED_PRICE = "fixedPrice"; /** * Unit cost node name. */ private static final String UNIT_COST = "unitCost"; /** * Unit price node name. */ private static final String UNIT_PRICE = "unitPrice"; /** * Total node name. */ private static final String TOTAL = "total"; /** * Nodes to use when a product template is selected. */ private static final ArchetypeNodes TEMPLATE_NODES = new ArchetypeNodes().exclude(QUANTITY, FIXED_PRICE, UNIT_PRICE, DISCOUNT, "clinician", TOTAL, DISPENSING, INVESTIGATIONS, REMINDERS, "batch"); /** * Constructs a {@link CustomerChargeActItemEditor}. * <p/> * This recalculates the tax amount. * * @param act the act to edit * @param parent the parent act * @param context the layout context */ public CustomerChargeActItemEditor(Act act, Act parent, LayoutContext context) { super(act, parent, context); if (!TypeHelper.isA(act, CustomerAccountArchetypes.INVOICE_ITEM, CustomerAccountArchetypes.CREDIT_ITEM, CustomerAccountArchetypes.COUNTER_ITEM)) { throw new IllegalArgumentException("Invalid act type:" + act.getArchetypeId().getShortName()); } setDisableDiscounts(getDisableDiscounts(getLocation())); dispensing = createDispensingCollectionEditor(); investigations = createCollectionEditor(INVESTIGATIONS, act); reminders = createCollectionEditor(REMINDERS, act); rules = ServiceHelper.getBean(StockRules.class); reminderRules = ServiceHelper.getBean(ReminderRules.class); quantityListener = new ModifiableListener() { public void modified(Modifiable modifiable) { updateMedicationQuantity(); } }; dispensingListener = new ModifiableListener() { public void modified(Modifiable modifiable) { updateQuantity(); updateBatch(); } }; if (dispensing != null) { dispensing.addModifiableListener(dispensingListener); } sellingUnits = LabelFactory.create(); if (act.isNew()) { // default the act start time to today act.setActivityStartTime(new Date()); } calculateTax(); ArchetypeNodes nodes = getFilterForProduct(getProductRef()); setArchetypeNodes(nodes); // add a listener to update the tax amount when the total changes totalListener = new ModifiableListener() { public void modified(Modifiable modifiable) { updateTaxAmount(); } }; getProperty(TOTAL).addModifiableListener(totalListener); // add a listener to update the discount amount when the quantity, // fixed or unit price changes. discountListener = new ModifiableListener() { public void modified(Modifiable modifiable) { updateDiscount(); } }; getProperty(FIXED_PRICE).addModifiableListener(discountListener); getProperty(QUANTITY).addModifiableListener(discountListener); getProperty(UNIT_PRICE).addModifiableListener(discountListener); getProperty(QUANTITY).addModifiableListener(quantityListener); startTimeListener = new ModifiableListener() { public void modified(Modifiable modifiable) { updatePatientActsStartTime(); } }; getProperty(START_TIME).addModifiableListener(startTimeListener); batchListener = new ModifiableListener() { @Override public void modified(Modifiable modifiable) { updateMedicationBatch(getStockLocationRef()); } }; } /** * Determines if an order has been placed for the item. * * @return {@code true} if an order has been placed */ public boolean isOrdered() { Property ordered = getProperty("ordered"); return ordered != null && ordered.getBoolean(); } /** * Returns the received quantity. * * @return the received quantity */ public BigDecimal getReceivedQuantity() { Property property = getProperty(RECEIVED_QUANTITY); return property != null ? property.getBigDecimal(BigDecimal.ZERO) : BigDecimal.ZERO; } /** * Sets the received quantity. * * @param quantity the received quantity */ public void setReceivedQuantity(BigDecimal quantity) { Property property = getProperty(RECEIVED_QUANTITY); if (property != null) { property.setValue(quantity); } } /** * Returns the returned quantity. * * @return the returned quantity */ public BigDecimal getReturnedQuantity() { Property property = getProperty(RETURNED_QUANTITY); return property != null ? property.getBigDecimal(BigDecimal.ZERO) : BigDecimal.ZERO; } /** * Sets the returned quantity. * * @param quantity the returned quantity */ public void setReturnedQuantity(BigDecimal quantity) { Property property = getProperty(RETURNED_QUANTITY); if (property != null) { property.setValue(quantity); } } /** * Disposes of the editor. * <br/> * Once disposed, the behaviour of invoking any method is undefined. */ @Override public void dispose() { super.dispose(); if (dispensing != null) { dispensing.removeModifiableListener(dispensingListener); } getProperty(TOTAL).removeModifiableListener(totalListener); getProperty(FIXED_PRICE).removeModifiableListener(discountListener); getProperty(QUANTITY).removeModifiableListener(discountListener); getProperty(UNIT_PRICE).removeModifiableListener(discountListener); getProperty(QUANTITY).removeModifiableListener(quantityListener); getProperty(START_TIME).removeModifiableListener(startTimeListener); } /** * Updates the discount and checks that it isn't less than the total cost. * <p/> * If so, gives the user the opportunity to remove the discount. * * @return {@code true} if the discount was updated */ @Override protected boolean updateDiscount() { boolean updated = super.updateDiscount(); BigDecimal discount = getProperty(DISCOUNT).getBigDecimal(BigDecimal.ZERO); if (updated && discount.compareTo(BigDecimal.ZERO) != 0) { BigDecimal fixedPriceMaxDiscount = getFixedPriceMaxDiscount(null); BigDecimal unitPriceMaxDiscount = getUnitPriceMaxDiscount(null); if ((fixedPriceMaxDiscount != null && !MathRules.equals(fixedPriceMaxDiscount, ONE_HUNDRED)) || (unitPriceMaxDiscount != null && !MathRules.equals(unitPriceMaxDiscount, ONE_HUNDRED))) { // if there is a fixed and/or unit price maximum discount present, and it is not 100%, check if the // sale price is less than the cost price BigDecimal quantity = getQuantity(); BigDecimal fixedCost = getFixedCost(); BigDecimal fixedPrice = getFixedPrice(); BigDecimal unitCost = getUnitCost(); BigDecimal unitPrice = getUnitPrice(); BigDecimal costPrice = fixedCost.add(unitCost.multiply(quantity)); BigDecimal salePrice = fixedPrice.add(unitPrice.multiply(quantity)); if (costPrice.compareTo(salePrice.subtract(discount)) > 0) { ConfirmationDialog dialog = new ConfirmationDialog( Messages.get("customer.charge.discount.title"), Messages.get("customer.charge.discount.message"), ConfirmationDialog.YES_NO); dialog.addWindowPaneListener(new PopupDialogListener() { @Override public void onYes() { getProperty(DISCOUNT).setValue(BigDecimal.ZERO); super.onYes(); } }); editorQueue.queue(dialog); } } } return updated; } /** * Validates the object. * * @param validator the validator * @return {@code true} if the object and its descendants are valid otherwise {@code false} */ @Override public boolean validate(Validator validator) { return (editorQueue == null || editorQueue.isComplete()) && super.validate(validator); } /** * Sets the popup editor manager. * * @param manager the popup editor manager */ public void setEditorQueue(EditorQueue manager) { editorQueue = manager; } /** * Returns the popup editor manager. * * @return the popup editor manager */ public EditorQueue getEditorQueue() { return editorQueue; } /** * Sets the prescriptions. * * @param prescriptions the prescriptions. May be {@code null} */ public void setPrescriptions(Prescriptions prescriptions) { this.prescriptions = prescriptions; if (dispensing != null) { dispensing.setPrescriptions(prescriptions); } } /** * Returns the prescriptions. * * @return the prescriptions. May be {@code null} */ public Prescriptions getPrescriptions() { return prescriptions; } /** * Determines if prescriptions should be prompted for. * * @param prompt if {@code true}, prompt for prescriptions, otherwise use them automatically */ public void setPromptForPrescriptions(boolean prompt) { promptForPrescription = prompt; } /** * Determines if prescription editing may be cancelled. * * @param cancel if {@code true}, prescription editing may be cancelled */ public void setCancelPrescription(boolean cancel) { cancelPrescription = cancel; } /** * Returns the reminders. * * @return the reminders */ public List<Act> getReminders() { return (reminders != null) ? reminders.getCurrentActs() : Collections.<Act>emptyList(); } /** * Returns a reference to the stock location. * * @return the stock location reference, or {@code null} if there is none */ public IMObjectReference getStockLocationRef() { return getParticipantRef("stockLocation"); } /** * Sets the charge context. * * @param context the charge context */ public void setChargeContext(ChargeContext context) { this.chargeContext = context; // register the context to delete acts after everything else has been saved to avoid Hibernate errors. if (dispensing != null) { dispensing.getEditor().setRemoveHandler(context); } if (investigations != null) { investigations.getEditor().setRemoveHandler(context); } if (reminders != null) { reminders.getEditor().setRemoveHandler(context); } } /** * Notifies the editor that the product has been ordered via a pharmacy. * <p/> * This refreshes the display to make the patient and product read-only, and display the received and returned * nodes. */ public void ordered() { updateLayout(getProduct()); } /** * Save any edits. * <p/> * This implementation saves the current object before children, to ensure deletion of child acts * don't result in StaleObjectStateException exceptions. * <p/> * This implementation will throw an exception if the product is an <em>product.template</em>. * Ideally, the act would be flagged invalid if this is the case, but template expansion only works for valid * acts. TODO * * @return {@code true} if the save was successful * @throws IllegalStateException if the product is a template */ @Override protected boolean doSave() { if (TypeHelper.isA(getObject(), CustomerAccountArchetypes.INVOICE_ITEM)) { if (chargeContext.getHistoryChanges() == null) { throw new IllegalStateException("PatientHistoryChanges haven't been registered"); } } return super.doSave(); } /** * Saves the object. * <p/> * For invoice items, this implementation also creates/deletes document acts related to the document templates * associated with the product, using {@link ChargeItemDocumentLinker}. * * @return {@code true} if the save was successful */ @Override protected boolean saveObject() { IArchetypeService service = ServiceHelper.getArchetypeService(); if (TypeHelper.isA(getObject(), CustomerAccountArchetypes.INVOICE_ITEM)) { if (chargeContext == null) { throw new IllegalStateException("PatientHistoryChanges haven't been registered"); } ChargeItemDocumentLinker linker = new ChargeItemDocumentLinker((FinancialAct) getObject(), service); linker.prepare(chargeContext.getHistoryChanges()); } return super.saveObject(); } /** * Returns the dispensing node editor. * * @return the editor. May be {@code null} */ protected ActRelationshipCollectionEditor getDispensingEditor() { return dispensing; } /** * Creates the layout strategy. * * @param fixedPrice the fixed price editor * @return a new layout strategy */ @Override protected IMObjectLayoutStrategy createLayoutStrategy(FixedPriceEditor fixedPrice) { return new CustomerChargeItemLayoutStrategy(fixedPrice); } /** * Determines if an editor should be disposed on layout change. * * @param editor the editor * @return {@code true} if the editor isn't for dispensing, investigations, or reminders */ @Override protected boolean disposeOnChangeLayout(Editor editor) { return editor != dispensing && editor != investigations && editor != reminders; } /** * Invoked when layout has completed. */ @Override protected void onLayoutCompleted() { super.onLayoutCompleted(); ProductParticipationEditor product = getProductEditor(); if (product != null) { // register the location in order to determine service ratios product.setLocation(getLocation()); } PatientParticipationEditor patient = getPatientEditor(); if (patient != null) { // add a listener to update the dispensing, investigation and reminder acts when the patient changes patient.addModifiableListener(new ModifiableListener() { public void modified(Modifiable modifiable) { updatePatientActsPatient(); } }); } ClinicianParticipationEditor clinician = getClinicianEditor(); if (clinician != null) { // add a listener to update the dispensing, investigation and reminder acts when the clinician changes clinician.addModifiableListener(new ModifiableListener() { public void modified(Modifiable modifiable) { updatePatientActsClinician(); } }); } updateBatch(getProduct(), getStockLocationRef()); } /** * Invoked when the product is changed, to update prices, dispensing and reminder acts. * * @param product the product. May be {@code null} */ @Override protected void productModified(Product product) { getProperty(FIXED_PRICE).removeModifiableListener(discountListener); getProperty(QUANTITY).removeModifiableListener(discountListener); getProperty(UNIT_PRICE).removeModifiableListener(discountListener); super.productModified(product); // update the layout if nodes require filtering updateLayout(product); updatePatientMedication(product); updateInvestigations(product); updateReminders(product); Property discount = getProperty(DISCOUNT); discount.setValue(BigDecimal.ZERO); Property fixedPrice = getProperty(FIXED_PRICE); Property unitPrice = getProperty(UNIT_PRICE); Property fixedCost = getProperty(FIXED_COST); Property unitCost = getProperty(UNIT_COST); if (TypeHelper.isA(product, TEMPLATE)) { fixedPrice.setValue(BigDecimal.ZERO); unitPrice.setValue(BigDecimal.ZERO); fixedCost.setValue(BigDecimal.ZERO); unitCost.setValue(BigDecimal.ZERO); updateSellingUnits(null); } else { ProductPrice fixedProductPrice = null; ProductPrice unitProductPrice = null; if (product != null) { fixedProductPrice = getDefaultFixedProductPrice(product); unitProductPrice = getDefaultUnitProductPrice(product); } if (fixedProductPrice != null) { fixedPrice.setValue(getPrice(product, fixedProductPrice)); fixedCost.setValue(getCost(fixedProductPrice)); } else { fixedPrice.setValue(BigDecimal.ZERO); fixedCost.setValue(BigDecimal.ZERO); } if (unitProductPrice != null) { unitPrice.setValue(getPrice(product, unitProductPrice)); unitCost.setValue(getCost(unitProductPrice)); } else { unitPrice.setValue(BigDecimal.ZERO); unitCost.setValue(BigDecimal.ZERO); } IMObjectReference stockLocation = updateStockLocation(product); updateSellingUnits(product); updateBatch(product, stockLocation); updateDiscount(); } notifyProductListener(product); getProperty(FIXED_PRICE).addModifiableListener(discountListener); getProperty(QUANTITY).addModifiableListener(discountListener); getProperty(UNIT_PRICE).addModifiableListener(discountListener); } /** * Calculates the tax amount. * * @throws ArchetypeServiceException for any archetype service error * @throws TaxRuleException for any tax error */ protected void calculateTax() { Party customer = getCustomer(); if (customer != null && getProductRef() != null) { FinancialAct act = (FinancialAct) getObject(); BigDecimal previousTax = act.getTaxAmount(); BigDecimal tax = calculateTax(customer); if (tax.compareTo(previousTax) != 0) { Property property = getProperty("tax"); property.refresh(); } } } /** * Calculates the tax amount. */ private void updateTaxAmount() { try { calculateTax(); } catch (OpenVPMSException exception) { ErrorHelper.show(exception); } } /** * Updates any patient acts with the start time. */ private void updatePatientActsStartTime() { Act parent = (Act) getObject(); for (PatientActEditor editor : getMedicationActEditors()) { editor.setStartTime(parent.getActivityStartTime()); } for (PatientInvestigationActEditor editor : getInvestigationActEditors()) { editor.setStartTime(parent.getActivityStartTime()); } for (ReminderEditor editor : getReminderEditors()) { editor.setStartTime(parent.getActivityStartTime()); } if (dispensing != null) { dispensing.refresh(); } if (investigations != null) { investigations.refresh(); } if (reminders != null) { reminders.refresh(); } } /** * Invoked when the product changes to update patient medications. * <p/> * If the new product is a medication and there is: * <ul> * <li>an existing act, the existing act will be updated. * <li>no existing act, a new medication will be created * </ul> * <p/> * If the product is null, any existing act will be removed * * @param product the product. May be {@code null} */ private void updatePatientMedication(Product product) { if (dispensing != null) { if (TypeHelper.isA(product, ProductArchetypes.MEDICATION)) { Set<PrescriptionMedicationActEditor> medicationEditors = getMedicationActEditors(); if (!medicationEditors.isEmpty()) { // set the product on the existing acts for (PrescriptionMedicationActEditor editor : medicationEditors) { editor.setProduct(product); changePrescription(editor); } dispensing.refresh(); } else { // add a new medication act Act act = (Act) dispensing.create(); if (act != null) { Act prescription = getPrescription(); if (prescription != null) { checkUsePrescription(prescription, product, act); } else { createMedicationEditor(product, act); } } } } else { // product is not a medication or is null. Remove any existing act for (Act act : dispensing.getCurrentActs()) { dispensing.remove(act); } } } } /** * Returns the prescription for the current patient and product, if one exists. * * @return the prescription, or {@code null} if none exists */ private Act getPrescription() { Party patient = getPatient(); Product product = getProduct(); return prescriptions != null && patient != null && product != null ? prescriptions.getPrescription(patient, product) : null; } /** * Determines if a prescription should be dispensed. * * @param prescription the prescription * @param product the product being dispensed * @param medication the medication act */ private void checkUsePrescription(final Act prescription, final Product product, final Act medication) { if (promptForPrescription) { ConfirmationDialog dialog = new ConfirmationDialog(Messages.get("customer.charge.prescription.title"), Messages.format("customer.charge.prescription.message", product.getName())); dialog.addWindowPaneListener(new PopupDialogListener() { @Override public void onOK() { createPrescriptionMedicationEditor(medication, prescription); } @Override public void onCancel() { createMedicationEditor(product, medication); } }); editorQueue.queue(dialog); } else { createPrescriptionMedicationEditor(medication, prescription); } } /** * Creates an editor for an <em>act.patientMedication</em> that dispenses a prescription. * * @param medication the medication * @param prescription the prescription */ private void createPrescriptionMedicationEditor(Act medication, Act prescription) { PrescriptionMedicationActEditor editor = dispensing.createEditor(medication, createLayoutContext(medication)); editor.setPrescription(prescription); dispensing.addEdited(editor); queuePatientActEditor(editor, false, cancelPrescription, dispensing); // queue editing of the act } /** * Creates an editor for an <em>act.patientMedication</em>. * * @param product the product * @param act the medication act */ private void createMedicationEditor(Product product, Act act) { boolean dispensingLabel = hasDispensingLabel(product); IMObjectEditor editor = createEditor(act, dispensing); dispensing.addEdited(editor); if (dispensingLabel) { // queue editing of the act queuePatientActEditor(editor, false, false, dispensing); } } /** * Invoked when the product changes to update investigation acts. * <p/> * This removes any existing investigations, and creates new ones, if required. * * @param product the product. May be {@code null} */ private void updateInvestigations(Product product) { if (investigations != null) { for (Act act : investigations.getCurrentActs()) { investigations.remove(act); } if (product != null) { // add a new investigation act for each investigation type (if any) for (Entity investigationType : getInvestigationTypes(product)) { Act act = (Act) investigations.create(); if (act != null) { IMObjectEditor editor = createEditor(act, investigations); if (editor instanceof PatientInvestigationActEditor) { PatientInvestigationActEditor investigationEditor = (PatientInvestigationActEditor) editor; investigationEditor.setInvestigationType(investigationType); investigationEditor.setProduct(product); } investigations.addEdited(editor); // queue editing of the act queuePatientActEditor(editor, true, false, investigations); } } } } } /** * Invoked when the product changes, to update reminders acts. * <p/> * This removes any existing reminders, and creates new ones, if required. * * @param product the product. May be {@code null} */ private void updateReminders(Product product) { if (reminders != null) { for (Act act : reminders.getCurrentActs()) { reminders.remove(act); } if (product != null) { Map<Entity, EntityRelationship> reminderTypes = getReminderTypes(product); for (Map.Entry<Entity, EntityRelationship> entry : reminderTypes.entrySet()) { Entity reminderType = entry.getKey(); EntityRelationship relationship = entry.getValue(); Act act = (Act) reminders.create(); if (act != null) { IMObjectEditor editor = createEditor(act, reminders); if (editor instanceof ReminderEditor) { ReminderEditor reminder = (ReminderEditor) editor; Date startTime = getStartTime(); reminder.setStartTime(startTime); reminder.setReminderType(reminderType); reminder.setPatient(getPatient()); reminder.setProduct(product); // override the due date calculated from the reminder type Date dueDate = reminderRules.calculateProductReminderDueDate(startTime, relationship); reminder.setEndTime(dueDate); } reminders.addEdited(editor); IMObjectBean bean = new IMObjectBean(relationship); boolean interactive = bean.getBoolean("interactive"); if (interactive) { // queue editing of the act queuePatientActEditor(editor, true, false, reminders); } } } } } } /** * Creates an act editor, with a new help context. * * @param act the act to editor * @param editors the editor collection * @return the editor */ private IMObjectEditor createEditor(Act act, ActRelationshipCollectionEditor editors) { return editors.createEditor(act, createLayoutContext(act)); } /** * Creates a layout context for editing an act. * * @param act the act being edited * @return a new layout context */ private LayoutContext createLayoutContext(Act act) { LayoutContext context = getLayoutContext(); return new DefaultLayoutContext(context, context.getHelpContext().topic(act, "edit")); } /** * Invoked when the product changes to update the layout, if required. * * @param product the product. May be {@code null} */ private void updateLayout(Product product) { ArchetypeNodes currentNodes = getArchetypeNodes(); IMObjectReference productRef = (product != null) ? product.getObjectReference() : null; ArchetypeNodes expectedFilter = getFilterForProduct(productRef); if (!ObjectUtils.equals(currentNodes, expectedFilter)) { Component popupFocus = null; if (editorQueue != null && !editorQueue.isComplete()) { popupFocus = FocusHelper.getFocus(); } changeLayout(expectedFilter); // this can move the focus away from the popups, if any if (editorQueue != null && editorQueue.isComplete()) { // no current popups, so move focus to the product moveFocusToProduct(); } else { // move the focus back to the popup FocusHelper.setFocus(popupFocus); } } } /** * Updates the selling units label. * * @param product the product. May be {@code null} */ private void updateSellingUnits(Product product) { String units = ""; if (product != null) { IMObjectBean bean = new IMObjectBean(product); String node = "sellingUnits"; if (bean.hasNode(node)) { units = LookupNameHelper.getName(product, node); } } sellingUnits.setText(units); } /** * Updates the batch. * * @param product the product. May be {@code null} * @param stockLocation the stock location. May be {@code null} */ private void updateBatch(Product product, IMObjectReference stockLocation) { BatchParticipationEditor batchEditor = getBatchEditor(); if (batchEditor != null) { try { batchEditor.removeModifiableListener(batchListener); batchEditor.setStockLocation(stockLocation); batchEditor.setProduct(product); updateMedicationBatch(stockLocation); } finally { batchEditor.addModifiableListener(batchListener); } } } /** * Helper to return the investigation types for a product. * <p/> * If there are multiple investigation types, these will be sorted on name. * * @param product the product * @return a list of investigation types */ private List<Entity> getInvestigationTypes(Product product) { List<Entity> result = Collections.emptyList(); EntityBean bean = new EntityBean(product); final String node = "investigationTypes"; if (bean.hasNode(node)) { result = bean.getNodeTargetEntities(node); Collections.sort(result, IMObjectSorter.getNameComparator(true)); } return result; } /** * Helper to return the reminder types and their relationships for a product. * <p/> * If there are multiple reminder types, these will be sorted on name. * * @param product the product * @return a the reminder type relationships */ private Map<Entity, EntityRelationship> getReminderTypes(Product product) { Map<EntityRelationship, Entity> map = reminderRules.getReminderTypes(product); Map<Entity, EntityRelationship> result = new TreeMap<Entity, EntityRelationship>( IMObjectSorter.getNameComparator(true)); for (Map.Entry<EntityRelationship, Entity> entry : map.entrySet()) { result.put(entry.getValue(), entry.getKey()); } return result; } /** * Queues an editor for display in a popup dialog. * Use this when there may be multiple editors requiring display. * <p/> * NOTE: all objects should be added to the collection prior to them being edited. If they are skipped, * they will subsequently be removed. This is necessary as the layout excludes nodes based on elements being * present. * * @param editor the editor to queue * @param skip if {@code true}, indicates that the editor may be skipped * @param cancel if {@code true}, indicates that the editor may be cancelled * @param collection the collection to remove the object from, if the editor is skipped */ private void queuePatientActEditor(final IMObjectEditor editor, boolean skip, boolean cancel, final ActRelationshipCollectionEditor collection) { if (editorQueue != null) { editorQueue.queue(editor, skip, cancel, new EditorQueue.Listener() { public void completed(boolean skipped, boolean cancelled) { if (skipped || cancelled) { collection.remove(editor.getObject()); } if (editorQueue.isComplete()) { moveFocusToProduct(); // force the parent collection editor to re-check the validation status of // this editor, in order for the Add button to be enabled. getListeners().notifyListeners(CustomerChargeActItemEditor.this); } } }); } } /** * Updates the medication quantity from the invoice. */ private void updateMedicationQuantity() { BigDecimal quantity = (BigDecimal) getProperty(QUANTITY).getValue(); if (dispensing != null && quantity != null) { dispensing.removeModifiableListener(dispensingListener); try { PatientMedicationActEditor editor = (PatientMedicationActEditor) dispensing.getCurrentEditor(); if (editor == null) { List<Act> acts = dispensing.getActs(); if (!acts.isEmpty()) { editor = (PatientMedicationActEditor) dispensing.getEditor(acts.get(0)); } } if (editor != null) { editor.setQuantity(quantity); dispensing.refresh(); } } finally { dispensing.addModifiableListener(dispensingListener); } } } /** * Updates the invoice quantity when a medication act changes. */ private void updateQuantity() { Property property = getProperty(QUANTITY); property.removeModifiableListener(quantityListener); try { if (dispensing != null) { Set<Act> acts = new HashSet<Act>(dispensing.getActs()); PatientMedicationActEditor current = (PatientMedicationActEditor) dispensing.getCurrentEditor(); if (current != null) { acts.add((Act) current.getObject()); } if (!acts.isEmpty()) { BigDecimal total = BigDecimal.ZERO; for (Act act : acts) { ActBean bean = new ActBean(act); BigDecimal quantity = bean.getBigDecimal(QUANTITY, BigDecimal.ZERO); total = total.add(quantity); } property.setValue(total); } dispensing.refresh(); } } finally { property.addModifiableListener(quantityListener); } } /** * Updates the medication batch from the invoice. * * @param stockLocation the stock location. May be {@code null} */ private void updateMedicationBatch(IMObjectReference stockLocation) { BatchParticipationEditor batchEditor = getBatchEditor(); if (batchEditor != null && dispensing != null) { dispensing.removeModifiableListener(dispensingListener); try { for (PatientMedicationActEditor editor : getMedicationActEditors()) { editor.setBatch(batchEditor.getEntity()); editor.setStockLocation(stockLocation); } dispensing.refresh(); } finally { dispensing.addModifiableListener(dispensingListener); } } } /** * Updates the batch if the medication batch changes. */ private void updateBatch() { Property batch = getProperty("batch"); if (batch != null) { batch.removeModifiableListener(batchListener); try { Entity selected = null; for (PatientMedicationActEditor editor : getMedicationActEditors()) { selected = editor.getBatch(); } getBatchEditor().setEntity(selected); } finally { batch.addModifiableListener(batchListener); } } } /** * Updates any child patient acts with the patient. */ private void updatePatientActsPatient() { IMObjectReference patient = getPatientRef(); for (PrescriptionMedicationActEditor editor : getMedicationActEditors()) { editor.setPatient(patient); changePrescription(editor); } for (PatientInvestigationActEditor editor : getInvestigationActEditors()) { editor.setPatient(patient); } for (ReminderEditor editor : getReminderEditors()) { editor.setPatient(patient); } } /** * Changes the prescription for an editor, if one is available. * * @param editor the editor */ private void changePrescription(final PrescriptionMedicationActEditor editor) { Act prescription = getPrescription(); if (prescription != null) { if (promptForPrescription) { Product product = getProduct(); String title = Messages.get("customer.charge.prescription.title"); String message = Messages.format("customer.charge.prescription.message", product.getName()); ConfirmationDialog dialog = new ConfirmationDialog(title, message); dialog.addWindowPaneListener(new PopupDialogListener() { @Override public void onOK() { editorQueue.queue(editor, true, cancelPrescription, null); } }); editorQueue.queue(dialog); } else { editorQueue.queue(editor, true, cancelPrescription, null); } } } /** * Updates any child patient acts with the clinician. */ private void updatePatientActsClinician() { IMObjectReference clinician = getClinicianRef(); for (PatientActEditor editor : getMedicationActEditors()) { editor.setClinician(clinician); } for (PatientInvestigationActEditor editor : getInvestigationActEditors()) { editor.setClinician(clinician); } } /** * Helper to move the focus to the product editor. */ private void moveFocusToProduct() { ProductParticipationEditor productEditor = getProductEditor(); if (productEditor != null) { FocusGroup group = productEditor.getFocusGroup(); if (group != null) { group.setFocus(); } } } /** * Returns editors for each of the <em>act.patientMedication</em> acts. * * @return the editors */ private Set<PrescriptionMedicationActEditor> getMedicationActEditors() { return getActEditors(dispensing); } /** * Returns the editors for each of the <em>act.patientInvestigation</em> acts. * * @return the editors */ private Set<PatientInvestigationActEditor> getInvestigationActEditors() { return getActEditors(investigations); } /** * Returns the editors for each of the <em>act.patientReminder</em> acts. * * @return the editors */ private Set<ReminderEditor> getReminderEditors() { return getActEditors(reminders); } /** * Returns the act editors for the specified collection editor. * * @param editors the collection editor. May be {@code null} * @return a set of editors */ @SuppressWarnings("unchecked") private <T extends IMObjectEditor> Set<T> getActEditors(ActRelationshipCollectionEditor editors) { Set<T> result = new HashSet<T>(); if (editors != null) { for (Act act : editors.getCurrentActs()) { T editor = (T) editors.getEditor(act); result.add(editor); } } return result; } /** * Determines if a medication product requires a dispensing label. * * @param product the product * @return {@code true} if the product requires a dispensing label */ private boolean hasDispensingLabel(Product product) { IMObjectBean bean = new IMObjectBean(product); return bean.getBoolean("label"); } /** * Updates the stock location associated with the product. * * @param product the new product * @return the stock location. May be {@code null} */ private IMObjectReference updateStockLocation(Product product) { Party stockLocation = null; if (TypeHelper.isA(product, MEDICATION, MERCHANDISE)) { Act parent = (Act) getParent(); if (parent != null) { ActBean bean = new ActBean(parent); Party location = (Party) getObject(bean.getNodeParticipantRef("location")); if (location != null) { stockLocation = rules.getStockLocation(product, location); } } } ActBean bean = new ActBean((Act) getObject()); if (stockLocation != null) { bean.setParticipant(STOCK_LOCATION_PARTICIPATION, stockLocation); } else { bean.removeParticipation(STOCK_LOCATION_PARTICIPATION); } return stockLocation != null ? stockLocation.getObjectReference() : null; } /** * Returns the value of the cost node of a price. * * @param price the product price * @return the cost */ private BigDecimal getCost(ProductPrice price) { IMObjectBean bean = new IMObjectBean(price); return bean.getBigDecimal("cost", BigDecimal.ZERO); } /** * Returns a node filter for the specified product reference. * <p/> * This excludes: * <ul> * <li>the dispensing node if the product isn't a <em>product.medication</em> * <li>the investigations node if the product isn't a <em>product.medication</em>, <em>product.merchandise</em>, * or <em>product.service</em> * <li>the reminders node is excluded if there are no reminders present. * <li>the discount node, if discounts are disabled</li> * </ul> * * @param product a reference to the product. May be {@code null} * @return a node filter for the product. If {@code null}, no nodes require filtering */ private ArchetypeNodes getFilterForProduct(IMObjectReference product) { ArchetypeNodes result = null; if (TypeHelper.isA(product, TEMPLATE)) { result = TEMPLATE_NODES; } else { List<String> filter = new ArrayList<String>(); filter.add(DISPENSING); filter.add(INVESTIGATIONS); filter.add(REMINDERS); if (getDisableDiscounts()) { filter.add(DISCOUNT); } boolean medication = TypeHelper.isA(product, MEDICATION); if (medication) { filter.remove(DISPENSING); } if (medication || TypeHelper.isA(product, MERCHANDISE, SERVICE)) { filter.remove(INVESTIGATIONS); } if (reminders != null && reminders.getCollection().size() > 0) { filter.remove(REMINDERS); } if (!filter.isEmpty()) { result = new ArchetypeNodes().exclude(filter); } if (isOrdered()) { if (result == null) { result = new ArchetypeNodes(); } result.simple(ORDER_NODES); } } return result; } /** * Helper to create a collection editor for an act relationship node, if the node exists. * <p/> * The returned editor is configured to not exclude default value objects. * * @param name the collection node name * @param act the act * @return the collection editor, or {@code null} if the node doesn't exist */ private ActRelationshipCollectionEditor createCollectionEditor(String name, Act act) { ActRelationshipCollectionEditor editor = null; CollectionProperty collection = (CollectionProperty) getProperty(name); if (collection != null && !collection.isHidden()) { editor = (ActRelationshipCollectionEditor) IMObjectCollectionEditorFactory.create(collection, act, getLayoutContext()); editor.setExcludeDefaultValueObject(false); getEditors().add(editor); } return editor; } /** * Creates an editor for the "dispensing" node. * * @return a new editor */ private DispensingActRelationshipCollectionEditor createDispensingCollectionEditor() { DispensingActRelationshipCollectionEditor editor = null; CollectionProperty collection = (CollectionProperty) getProperty("dispensing"); if (collection != null && !collection.isHidden()) { editor = new DispensingActRelationshipCollectionEditor(collection, (Act) getObject(), getLayoutContext()); getEditors().add(editor); } return editor; } /** * Returns the product batch participation editor. * * @return the product batch participation, or {@code null} if none exists */ protected BatchParticipationEditor getBatchEditor() { return getBatchEditor(true); } /** * Returns the product batch participation editor. * * @param create if {@code true} force creation of the edit components if it hasn't already been done * @return the product batch participation, or {@code null} if none exists */ protected BatchParticipationEditor getBatchEditor(boolean create) { ParticipationEditor<Entity> editor = getParticipationEditor("batch", create); return (BatchParticipationEditor) editor; } protected class CustomerChargeItemLayoutStrategy extends PriceItemLayoutStrategy { public CustomerChargeItemLayoutStrategy(FixedPriceEditor fixedPrice) { super(fixedPrice); if (dispensing != null) { addComponent(new ComponentState(dispensing)); } if (investigations != null) { addComponent(new ComponentState(investigations)); } if (reminders != null) { addComponent(new ComponentState(reminders)); } } @Override protected ComponentState createComponent(Property property, IMObject parent, LayoutContext context) { ComponentState state; String name = property.getName(); if (QUANTITY.equals(name)) { state = super.createComponent(property, parent, context); Component component = RowFactory.create(Styles.CELL_SPACING, state.getComponent(), sellingUnits); state = new ComponentState(component, property); } else if (("patient".equals(name) || "product".equals(name)) && isOrdered()) { // the item has been ordered via an HL7 pharmacy. The patient and product cannot be changed state = super.createComponent(createReadOnly(property), parent, context); } else { state = super.createComponent(property, parent, context); } return state; } } }